fix(web): indent subcategories in SetCategoryDialog#636
Conversation
WalkthroughDerives a Changes
Sequence Diagram(s)sequenceDiagram
participant Parent as Parent Component
participant Cap as useInstanceCapabilities
participant Meta as Instance Metadata
participant SetDlg as SetCategoryDialog
participant Tree as CategoryTree
Parent->>Cap: fetch capabilities
Parent->>Meta: read preferences
Cap-->>Parent: { supportsSubcategories }
Meta-->>Parent: { use_subcategories }
Parent->>Parent: allowSubcategories = supportsSubcategories && use_subcategories
Parent->>SetDlg: open(..., useSubcategories=allowSubcategories)
alt allowSubcategories == true
SetDlg->>Tree: buildCategoryTree(availableCategories)
Tree-->>SetDlg: hierarchical nodes
SetDlg->>SetDlg: flatten visible nodes (name, displayName, level)
SetDlg-->>Parent: selected category (category.name)
else allowSubcategories == false
SetDlg->>SetDlg: filter/render flat category list
SetDlg-->>Parent: selected category (flat name)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧬 Code graph analysis (2)web/src/components/torrents/TorrentManagementBar.tsx (1)
web/src/components/torrents/TorrentCardsMobile.tsx (2)
🪛 Biome (2.1.2)web/src/components/torrents/TorrentManagementBar.tsx[error] 109-109: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render. Hooks should not be called after an early return. For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order. (lint/correctness/useHookAtTopLevel) ⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (5)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/components/torrents/TorrentManagementBar.tsx (1)
93-113: Critical: Hook called after early return violates Rules of Hooks.The early return at lines 93-95 causes
useInstanceCapabilities(and other hooks below it) to be called conditionally. React requires hooks to be called in the same order on every render. WheninstanceIdis invalid, the component returns early, but on subsequent renders with a validinstanceId, hooks will be called in a different order.This can cause React to lose track of hook state, leading to bugs or crashes.
Move the guard logic after all hooks, or restructure to avoid conditional hook invocation:
export const TorrentManagementBar = memo(function TorrentManagementBar({ instanceId, selectedHashes = [], selectedTorrents = [], isAllSelected = false, totalSelectionCount = 0, totalSelectionSize = 0, filters, search, excludeHashes = [], onComplete, }: TorrentManagementBarProps) { - if (typeof instanceId !== "number" || instanceId <= 0) { - return null - } + const isValidInstanceId = typeof instanceId === "number" && instanceId > 0 const selectionCount = totalSelectionCount || selectedHashes.length // Use shared metadata hook to leverage cache from table and filter sidebar - const { data: metadata, isLoading: isMetadataLoading } = useInstanceMetadata(instanceId) + const { data: metadata, isLoading: isMetadataLoading } = useInstanceMetadata(isValidInstanceId ? instanceId : 0) const availableTags = metadata?.tags || [] const availableCategories = metadata?.categories || {} const preferences = metadata?.preferences const isLoadingTagsData = isMetadataLoading && availableTags.length === 0 const isLoadingCategoriesData = isMetadataLoading && Object.keys(availableCategories).length === 0 // Get capabilities to check subcategory support - const { data: capabilities } = useInstanceCapabilities(instanceId) + const { data: capabilities } = useInstanceCapabilities(isValidInstanceId ? instanceId : null) const supportsSubcategories = capabilities?.supportsSubcategories ?? false const allowSubcategories = Boolean( supportsSubcategories && (preferences?.use_subcategories ?? false) ) // ... rest of hooks ... + // Keep this guard after hooks so their invocation order stays stable. + if (!isValidInstanceId) { + return null + }Alternatively, use the
enabledoption inuseInstanceCapabilitiesto disable the query wheninstanceIdis invalid, ensuring all hooks are still called.
🧹 Nitpick comments (3)
web/src/components/torrents/TorrentDialogs.tsx (1)
31-31: Unused import:Torrenttype is imported but not used in this file.The
Torrenttype is imported alongsideCategory, but onlyCategoryis used in theSetCategoryDialogPropsinterface.-import type { Category, Torrent } from "@/types" +import type { Category } from "@/types"web/src/components/torrents/TorrentCardsMobile.tsx (2)
1067-1071: Subcategory flags: consider unifying or documentingbackendUseSubcategoriesvsallowSubcategories.You now have two related booleans:
backendUseSubcategoriesfromuseTorrentsList(data-layer view).allowSubcategoriesfrom capabilities +preferences?.use_subcategories, used forSetCategoryDialog.If these are intended to represent the same “effective” behavior, it may be clearer to derive a single
effectiveUseSubcategoriesand use it both for the callback andSetCategoryDialog. If they are intentionally different (e.g., backend vs. user-facing gating), a brief comment explaining the distinction would help future maintainers reason about when each should be used.Also applies to: 1187-1193, 2148-2157
83-83:useCrossSeedFilterwith optionalonFilterChange: ensure the hook toleratesundefined.
onFilterChangeis optional in props but is passed directly intouseCrossSeedFilter. The button that callsfilterCrossSeedsis already guarded behindonFilterChange && …, so the callback won’t be invoked without it, but the hook itself still seesonFilterChange(possiblyundefined).Confirm that
useCrossSeedFilterhandles a missingonFilterChangegracefully (e.g., checks before calling) rather than assuming it’s always defined. If not, consider either:
- Making
onFilterChangerequired inTorrentCardsMobileProps, or- Only calling
useCrossSeedFilterwhenonFilterChangeis present, or having the hook default to a no‑op when it’s not.Also applies to: 1620-1623, 1927-1940
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
web/src/components/torrents/TorrentCardsMobile.tsx(7 hunks)web/src/components/torrents/TorrentDialogs.tsx(6 hunks)web/src/components/torrents/TorrentManagementBar.tsx(3 hunks)web/src/components/torrents/TorrentTableOptimized.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
web/src/components/torrents/TorrentManagementBar.tsx (1)
web/src/hooks/useInstanceCapabilities.ts (1)
useInstanceCapabilities(15-34)
web/src/components/torrents/TorrentCardsMobile.tsx (2)
web/src/types/index.ts (3)
Torrent(172-229)TorrentCounts(259-265)Category(310-313)internal/qbittorrent/sync_manager.go (1)
TorrentCounts(1657-1663)
🪛 Biome (2.1.2)
web/src/components/torrents/TorrentManagementBar.tsx
[error] 109-109: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (8)
web/src/components/torrents/TorrentDialogs.tsx (3)
1106-1166: Well-structured category filtering with subcategory support.The tree-building and filtering logic correctly handles:
- Caching via
Mapto avoid redundant recursive calls- Inclusive filtering that shows parent nodes when children match the search query
- Proper flattening of the tree structure with level information for indentation
The fallback path for non-subcategory mode maintains backward compatibility.
1250-1264: Virtualized category rendering correctly applies hierarchical indentation.The implementation properly:
- Uses
category.namefor selection and comparison- Applies
paddingLeftbased oncategory.levelfor visual hierarchy- Includes
titleattribute for accessibility on truncated names
1272-1288: Non-virtualized category rendering mirrors the virtualized path correctly.Both rendering paths maintain consistent behavior for indentation and selection logic.
web/src/components/torrents/TorrentManagementBar.tsx (1)
670-670: Correct propagation ofuseSubcategoriesto SetCategoryDialog.The
allowSubcategoriesflag is correctly derived from both capability support and user preference, then passed to the dialog.web/src/components/torrents/TorrentTableOptimized.tsx (3)
1000-1002: Correct derivation ofallowSubcategoriesflag.The logic correctly combines:
- Backend capability support (
supportsSubcategories)- User preference (
preferences?.use_subcategories)- Fallback to data-driven flag (
subcategoriesFromData)This ensures subcategory features are only enabled when both supported and configured.
2510-2510: Consistent propagation ofuseSubcategoriesto TorrentContextMenu.Both compact and normal view paths correctly pass
allowSubcategoriesto the context menu, ensuring consistent subcategory behavior across view modes.Also applies to: 2609-2609
2951-2951: SetCategoryDialog receivesuseSubcategoriesprop.The dialog correctly receives
allowSubcategoriesto enable hierarchical category display when supported.web/src/components/torrents/TorrentCardsMobile.tsx (1)
311-323: Upstream types are properly updated across all components—no TypeScript signature mismatches.Verification confirms that all related type definitions and implementations have already been updated to include the trailing optional
useSubcategoriesparameter:
- TorrentTableResponsive.tsx (line 20): callback signature includes
useSubcategories?: boolean✓- TorrentTableOptimized.tsx (line 601): callback signature includes
useSubcategories?: boolean✓- TorrentTableOptimized.tsx (line 1120): passes
nextUseSubcategoriesas the 6th argument ✓- Torrents.tsx (line 101): handler
handleFilteredDataUpdateacceptssubcategoriesEnabled?: boolean✓The implementation is consistent and type-safe throughout. No updates are needed.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
web/src/components/torrents/TorrentCardsMobile.tsx (1)
1190-1195: Remove redundant Boolean() wrapper.The expression
supportsSubcategories && (preferences?.use_subcategories ?? subcategoriesFromData ?? false)already resolves to a boolean value. The outerBoolean()wrapper is unnecessary.Apply this diff:
- const allowSubcategories = Boolean( - supportsSubcategories && (preferences?.use_subcategories ?? subcategoriesFromData ?? false) - ) + const allowSubcategories = supportsSubcategories && (preferences?.use_subcategories ?? subcategoriesFromData ?? false)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/src/components/torrents/TorrentCardsMobile.tsx(7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/components/torrents/TorrentCardsMobile.tsx (2)
web/src/types/index.ts (3)
Torrent(172-229)TorrentCounts(259-265)Category(310-313)internal/qbittorrent/sync_manager.go (1)
TorrentCounts(1657-1663)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (5)
web/src/components/torrents/TorrentCardsMobile.tsx (5)
60-60: LGTM - Missing imports added for existing functionality.These imports support existing features (cross-seed filtering) that were already implemented but had missing imports. Good catch fixing these while updating the file.
Also applies to: 83-83
318-318: Callback signature correctly extended for subcategory support.The
useSubcategoriesparameter receives backend-derived subcategory state, which is appropriate for data filtering callbacks. This differs from the UI-levelallowSubcategories(which combines backend capabilities with user preferences) that's passed to dialog components.
1070-1070: LGTM - Preferences extracted correctly.Proper use of optional chaining to safely access preferences from instance metadata.
1198-1203: LGTM - Callback correctly invoked with backend subcategory state.The effect properly passes
subcategoriesFromData(backend state) to the callback and includes it in the dependency array. This ensures parent components receive data updates when subcategory configuration changes on the backend.
2158-2158: LGTM - Dialog correctly receives UI-level subcategory preference.Passing
allowSubcategories(which respects both backend capabilities and user preferences) enables SetCategoryDialog to render indented subcategories when appropriate. This aligns with the PR objective to fix subcategory indentation in the dialog.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.7.0` -> `v1.8.1` | --- ### Release Notes <details> <summary>autobrr/qui (ghcr.io/autobrr/qui)</summary> ### [`v1.8.1`](https://github.com/autobrr/qui/releases/tag/v1.8.1) [Compare Source](autobrr/qui@v1.8.0...v1.8.1) #### Changelog ##### Bug Fixes - [`61c87e1`](autobrr/qui@61c87e1): fix(torznab): use detached context for indexer tests ([#​659](autobrr/qui#659)) ([@​s0up4200](https://github.com/s0up4200)) **Full Changelog**: <autobrr/qui@v1.8.0...v1.8.1> #### Docker images - `docker pull ghcr.io/autobrr/qui:v1.8.1` - `docker pull ghcr.io/autobrr/qui:latest` #### What to do next? - Join our [Discord server](https://discord.autobrr.com/qui) Thank you for using qui! ### [`v1.8.0`](https://github.com/autobrr/qui/releases/tag/v1.8.0) [Compare Source](autobrr/qui@v1.7.0...v1.8.0) #### Changelog ##### New Features - [`6903812`](autobrr/qui@6903812): feat(crossseed): batch torrent file lookups end-to-end ([#​625](autobrr/qui#625)) ([@​s0up4200](https://github.com/s0up4200)) - [`336ce48`](autobrr/qui@336ce48): feat(crossseed): persist seeded search settings ([#​618](autobrr/qui#618)) ([@​s0up4200](https://github.com/s0up4200)) - [`7b0b292`](autobrr/qui@7b0b292): feat(docker): add curl to Dockerfiles ([#​570](autobrr/qui#570)) ([@​onedr0p](https://github.com/onedr0p)) - [`91e1677`](autobrr/qui@91e1677): feat(filters): default-hide empty status/category/tag groups ([#​581](autobrr/qui#581)) ([@​s0up4200](https://github.com/s0up4200)) - [`f07bb8d`](autobrr/qui@f07bb8d): feat(header): add missing links to header burger menu ([#​624](autobrr/qui#624)) ([@​nuxencs](https://github.com/nuxencs)) - [`ee4c16b`](autobrr/qui@ee4c16b): feat(instances): allow disabling qbit instances ([#​582](autobrr/qui#582)) ([@​s0up4200](https://github.com/s0up4200)) - [`477db14`](autobrr/qui@477db14): feat(search): column filters ([#​633](autobrr/qui#633)) ([@​nuxencs](https://github.com/nuxencs)) - [`cd6db45`](autobrr/qui@cd6db45): feat(themes): add basic variation support ([#​569](autobrr/qui#569)) ([@​jabloink](https://github.com/jabloink)) - [`979a0d4`](autobrr/qui@979a0d4): feat(torrents): add clear filters action for empty filtered state ([#​627](autobrr/qui#627)) ([@​s0up4200](https://github.com/s0up4200)) - [`e06acb7`](autobrr/qui@e06acb7): feat(torrents): add cross-seeding and search ([#​553](autobrr/qui#553)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`95cef23`](autobrr/qui@95cef23): feat(torrents): add reannounce monitor ([#​606](autobrr/qui#606)) ([@​s0up4200](https://github.com/s0up4200)) - [`098fdb0`](autobrr/qui@098fdb0): feat(torrents): add rename functionality in TorrentDetailsPanel ([#​590](autobrr/qui#590)) ([@​s0up4200](https://github.com/s0up4200)) - [`6e8fdbd`](autobrr/qui@6e8fdbd): feat(torrents): implement drag-and-drop file upload to add torrents ([#​568](autobrr/qui#568)) ([@​dthinhle](https://github.com/dthinhle)) - [`9240545`](autobrr/qui@9240545): feat(ui): add dense view mode for compact table display ([#​643](autobrr/qui#643)) ([@​s0up4200](https://github.com/s0up4200)) - [`77fad15`](autobrr/qui@77fad15): feat(ui): improve torrent details panel file tree and rename UX ([#​650](autobrr/qui#650)) ([@​s0up4200](https://github.com/s0up4200)) - [`8b1e70e`](autobrr/qui@8b1e70e): feat(web): Use original qBittorrent status names ([#​595](autobrr/qui#595)) ([@​FibreTTP](https://github.com/FibreTTP)) - [`01dd553`](autobrr/qui@01dd553): feat(web): show listening port in connectable status tooltip ([#​635](autobrr/qui#635)) ([@​s0up4200](https://github.com/s0up4200)) - [`3140739`](autobrr/qui@3140739): feat: make tracker icon column sortable ([#​513](autobrr/qui#513)) ([@​s0up4200](https://github.com/s0up4200)) ##### Bug Fixes - [`240b40d`](autobrr/qui@240b40d): fix(auth): avoid logout on license activation errors ([#​602](autobrr/qui#602)) ([@​s0up4200](https://github.com/s0up4200)) - [`7185408`](autobrr/qui@7185408): fix(backups): do not persist ZIPs to disk ([#​632](autobrr/qui#632)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`de0e00a`](autobrr/qui@de0e00a): fix(content): use Hints for detection ([#​621](autobrr/qui#621)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`5f016a8`](autobrr/qui@5f016a8): fix(cross): performance improvements ([#​629](autobrr/qui#629)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`82c74ba`](autobrr/qui@82c74ba): fix(crossseed): flip deduplication to maps ([#​622](autobrr/qui#622)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`b78a079`](autobrr/qui@b78a079): fix(crossseed): inherit TMM state from matched torrent ([#​654](autobrr/qui#654)) ([@​s0up4200](https://github.com/s0up4200)) - [`2438fc6`](autobrr/qui@2438fc6): fix(crossseed): process full RSS feeds ([#​615](autobrr/qui#615)) ([@​s0up4200](https://github.com/s0up4200)) - [`6f57090`](autobrr/qui@6f57090): fix(database): do not release mutex on tx err ([#​571](autobrr/qui#571)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`74509d4`](autobrr/qui@74509d4): fix(incognito): prevent categories leaking ([#​592](autobrr/qui#592)) ([@​s0up4200](https://github.com/s0up4200)) - [`f08eff2`](autobrr/qui@f08eff2): fix(instances): support empty username for localhost bypass ([#​575](autobrr/qui#575)) ([@​s0up4200](https://github.com/s0up4200)) - [`cd3caaf`](autobrr/qui@cd3caaf): fix(license): cap 7d offline grace, ignore transient errors ([#​617](autobrr/qui#617)) ([@​s0up4200](https://github.com/s0up4200)) - [`59c747b`](autobrr/qui@59c747b): fix(reannounce): validate number fields and show min hints ([#​613](autobrr/qui#613)) ([@​s0up4200](https://github.com/s0up4200)) - [`f6bd1e6`](autobrr/qui@f6bd1e6): fix(themes): correct Nightwalker description from purple to blue ([#​648](autobrr/qui#648)) ([@​s0up4200](https://github.com/s0up4200)) - [`2b641c5`](autobrr/qui@2b641c5): fix(torznab): filter Prowlarr autodiscovery to enabled torrent indexers ([#​638](autobrr/qui#638)) ([@​s0up4200](https://github.com/s0up4200)) - [`1995783`](autobrr/qui@1995783): fix(ui): improve cross-seed mobile responsiveness ([#​647](autobrr/qui#647)) ([@​s0up4200](https://github.com/s0up4200)) - [`b83aebe`](autobrr/qui@b83aebe): fix(web): align CrossSeedDialog indexers with search flows ([#​619](autobrr/qui#619)) ([@​s0up4200](https://github.com/s0up4200)) - [`3b60821`](autobrr/qui@3b60821): fix(web): indent subcategories in SetCategoryDialog ([#​636](autobrr/qui#636)) ([@​s0up4200](https://github.com/s0up4200)) - [`82850cd`](autobrr/qui@82850cd): fix: glob pattern formatting in tooltip content ([#​579](autobrr/qui#579)) ([@​onedr0p](https://github.com/onedr0p)) ##### Other Changes - [`c20bc0a`](autobrr/qui@c20bc0a): build(vite): enable default minification ([#​574](autobrr/qui#574)) ([@​s0up4200](https://github.com/s0up4200)) - [`ceac8ca`](autobrr/qui@ceac8ca): chore(ci): upgrade Claude Code workflow to Opus 4.5 ([@​s0up4200](https://github.com/s0up4200)) - [`9d6c10e`](autobrr/qui@9d6c10e): chore(deps): bump actions/checkout from 5 to 6 in the github group ([#​628](autobrr/qui#628)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`f5704de`](autobrr/qui@f5704de): chore(deps): bump golang.org/x/crypto from 0.43.0 to 0.45.0 ([#​611](autobrr/qui#611)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`0aae9aa`](autobrr/qui@0aae9aa): chore(deps): bump the golang group with 3 updates ([#​546](autobrr/qui#546)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`0d97087`](autobrr/qui@0d97087): chore(themes): add crypto instructions in-app ([#​620](autobrr/qui#620)) ([@​s0up4200](https://github.com/s0up4200)) - [`e778865`](autobrr/qui@e778865): docs(funding): add donation methods and crypto addresses ([#​583](autobrr/qui#583)) ([@​s0up4200](https://github.com/s0up4200)) - [`563645c`](autobrr/qui@563645c): docs: update qui image ([#​655](autobrr/qui#655)) ([@​s0up4200](https://github.com/s0up4200)) **Full Changelog**: <autobrr/qui@v1.7.0...v1.8.0> #### Docker images - `docker pull ghcr.io/autobrr/qui:v1.8.0` - `docker pull ghcr.io/autobrr/qui:latest` #### What to do next? - Join our [Discord server](https://discord.autobrr.com/qui) Thank you for using qui! </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi41LjAiLCJ1cGRhdGVkSW5WZXIiOiI0Mi41LjAiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbImltYWdlIl19--> Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/2211 Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net> Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Closes #598
Summary by CodeRabbit
New Features
Performance
✏️ Tip: You can customize this high-level summary in your review settings.