Skip to content

feat: bulk video actions#1052

Merged
Zibbp merged 3 commits intomainfrom
bulk-video-actions
Feb 8, 2026
Merged

feat: bulk video actions#1052
Zibbp merged 3 commits intomainfrom
bulk-video-actions

Conversation

@Zibbp
Copy link
Owner

@Zibbp Zibbp commented Feb 7, 2026

Add bulk video actions on the Video Grid component.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

Walkthrough

Adds per-video selection and bulk-action support across the videos UI: new selection checkbox styling and props on VideoCard, selection state and bulk actions (menus, modals, concurrent mutations) in VideoGrid, conditional cache-invalidation flags in playback/videos hooks, and new i18n strings for bulk UI in en/de/uk.

Changes

Cohort / File(s) Summary
Video Card — selection UI & styles
frontend/app/components/videos/Card.tsx, frontend/app/components/videos/Card.module.css
Adds .selectionCheckboxInline CSS and makes VideoCard optionally selectable via new props (selectable, selected, onSelectionChange); renders a Mantine Checkbox when enabled.
Video Grid — selection state & bulk actions
frontend/app/components/videos/Grid.tsx
Introduces enableSelection prop, per-video selection state, select-all controls, selected count/clear button, bulk actions Menu, bulk operation orchestration (concurrent mutations, loading state, notifications, cache invalidation), and two modals (playlist add, multi-delete). Passes selection props to VideoCard.
Hooks — conditional cache invalidation
frontend/app/hooks/usePlayback.ts, frontend/app/hooks/useVideos.ts
Adds optional flags (invalidatePlaybackQuery?, invalidateVideoQueries?) to mutation inputs and changes onSuccess handlers to conditionally skip or run broader query invalidation (videos, channel_videos, playlist_videos, search).
i18n — bulk UI strings
frontend/messages/en.json, frontend/messages/de.json, frontend/messages/uk.json
Adds numerous localization keys for bulk action UI: tooltips, checkbox labels, select-all/count/clear labels, bulk actions button/menu entries, modal titles, and bulk operation notifications (including plural videosLockedNotification).
Minor formatting / imports
frontend/app/components/videos/ChannelVideos.tsx
Reformatted import list and added EOF newline; no behavioral changes.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: bulk video actions' clearly and concisely summarizes the main change—adding bulk action functionality to video operations.
Description check ✅ Passed The description 'Add bulk video actions on the Video Grid component' directly relates to the changeset, which implements bulk selection and bulk actions across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bulk-video-actions

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
frontend/app/components/videos/Grid.tsx (4)

188-190: Unbounded concurrent requests may overwhelm the server.

Promise.allSettled(selectedVideoList.map(...)) fires one HTTP request per selected video simultaneously. If a user selects 50–100+ videos across pages, this sends that many parallel requests at once, which can overload the backend or trigger rate limiting.

Consider batching with a concurrency limit (e.g., using a small helper or p-limit):

Example with p-limit
+import pLimit from "p-limit";
+
+const BULK_CONCURRENCY = 5;
+
 const runBulkOperation = async (
   operation: (video: T) => Promise<unknown>,
   successMessage: string,
   onSuccess?: () => Promise<void> | void
 ) => {
   if (selectedVideoList.length === 0) return;
   try {
     setBulkActionLoading(true);
+    const limit = pLimit(BULK_CONCURRENCY);
     const results = await Promise.allSettled(
-      selectedVideoList.map((video) => Promise.resolve().then(() => operation(video)))
+      selectedVideoList.map((video) => limit(() => operation(video)))
     );

203-219: Notification title shows successMessage even on partial failure.

When some operations fail (failureCount > 0 && failureCount < results.length), the title falls through to successMessage (line 209), which can be misleading. Consider a distinct title for the partial-failure case:

Suggested simplification
       showNotification({
         title:
           failureCount === 0
             ? successMessage
             : failureCount === results.length
               ? t("error")
-              : successMessage,
+              : t("partialSuccess"),

189-189: Redundant Promise.resolve().then(...) wrapping.

Promise.allSettled already handles both synchronous exceptions and rejected promises from operation(video). The Promise.resolve().then(() => ...) wrapper adds no value.

Simplification
     const results = await Promise.allSettled(
-      selectedVideoList.map((video) => Promise.resolve().then(() => operation(video)))
+      selectedVideoList.map((video) => operation(video))
     );

543-551: Multi-delete modal onClose does not clear selection, but content's handleClose does.

Closing the modal via ESC/overlay calls closeMultiDeleteModal (line 545), preserving selections. The content component receives handleCloseMultiDeleteModal (line 549), which clears selections. This asymmetry is fine if intentional — dismissing preserves selection while successful deletion clears it — but worth a brief comment in the code so future maintainers understand the distinction.


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

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
frontend/messages/en.json (1)

406-412: ⚠️ Potential issue | 🟡 Minor

Typo: "Mark was unwatched" → "Mark as unwatched".

Line 408 has a typo in AdminVideosPage.bulkActionMenu.markAsUnwatched.

📝 Proposed fix
       "markAsWatched": "Mark as watched",
-      "markAsUnwatched": "Mark was unwatched",
+      "markAsUnwatched": "Mark as unwatched",
       "lock": "Lock videos",
🤖 Fix all issues with AI agents
In `@frontend/app/components/videos/Grid.tsx`:
- Around line 325-389: The bulk action Menu allows triggering new actions while
bulkActionLoading is true because individual Menu.Item handlers (e.g.,
handleMarkVideosAsWatched, handleMarkVideosAsUnwatched, handleLockVideos,
handleGenerateStaticThumbnails, handleGenerateSpriteThumbnails,
openPlaylistModal, openMultiDeleteModal) are still clickable once the menu is
open; fix this by disabling each Menu.Item when bulkActionLoading is true (add
disabled={bulkActionLoading} to the Menu.Item elements) and optionally close the
menu when a bulk action starts by controlling the Menu open state (introduce a
menuOpened state and call setMenuOpened(false) at the start of each handler),
ensuring all handlers are guarded by bulkActionLoading checks.

In `@frontend/messages/de.json`:
- Line 769: The German string uses the wrong verb form: update the localization
key "unlocked" (used by "videosLockedNotification") from the infinitive
"entsperren" to the correct past participle "entsperrt" so the sentence reads
"Videos wurden entsperrt."; apply the same replacement for the "unlocked" entry
referenced in VideoComponents to keep both translations consistent.
🧹 Nitpick comments (5)
frontend/app/hooks/useVideos.ts (1)

488-515: Broadened cache invalidation scope looks correct.

The expanded invalidation (videos, channel_videos, playlist_videos, search) ensures all views stay consistent after lock/unlock, and Promise.all is appropriate here.

Minor style inconsistency: this uses early-return (=== false → return) while usePlayback.ts uses the inverse guard (!== false → invalidate). Both work identically, but unifying the style would improve readability.

frontend/app/components/videos/Grid.tsx (4)

179-204: Unbounded concurrency in bulk operations could overwhelm the backend.

Promise.all fires every mutation simultaneously. If a user selects 50–100+ videos, this sends that many parallel requests at once, which could trigger rate limits or overload the server.

Consider batching with a concurrency limit (e.g., using a simple chunked approach or a library like p-limit).

Also, partial failures are silently swallowed—some mutations may succeed before one throws, but only the error is surfaced. Consider tracking and reporting partial success/failure counts.

Example: simple chunked approach
  const runBulkOperation = async (
    operation: (video: T) => Promise<unknown>,
    successMessage: string,
    onSuccess?: () => Promise<void> | void
  ) => {
    if (selectedVideoList.length === 0) return;
+   const BATCH_SIZE = 5;
    try {
      setBulkActionLoading(true);
-     await Promise.all(selectedVideoList.map((video) => operation(video)));
+     for (let i = 0; i < selectedVideoList.length; i += BATCH_SIZE) {
+       const batch = selectedVideoList.slice(i, i + BATCH_SIZE);
+       await Promise.all(batch.map((video) => operation(video)));
+     }
      if (onSuccess) {
        await onSuccess();
      }

67-75: Selection state persists across page navigations — could surprise users.

selectedVideos is never cleared when the user changes pages (via onPageChange). This means videos selected on page 1 remain selected when navigating to page 2. While cross-page selection can be useful, the "Select all on page" checkbox and count label may confuse users who don't realize they have off-screen selections.

Consider either: (a) clearing selection on page change, or (b) making the cross-page behavior explicit in the UI (e.g., showing "X selected across pages").


279-282: Inconsistent selection clearing between modals.

handleCloseMultiDeleteModal clears selectedVideos (Line 281), but closing the playlist modal (closePlaylistModal, Line 457) does not. After a successful bulk playlist add, stale selections persist. Consider clearing selection after playlist add as well for consistency, or make it intentional so users can chain multiple bulk actions on the same selection.

Also applies to: 455-473


37-53: The component is accumulating significant bulk-action logic — consider extracting a custom hook.

The selection state, bulk operation runner, and individual action handlers add ~150 lines of logic to this component. Extracting a useBulkVideoActions(videos) hook would improve readability and testability of both the grid layout and the bulk action logic.

Also applies to: 55-81, 153-277

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@frontend/app/components/videos/Grid.tsx`:
- Around line 180-205: The runBulkOperation function currently uses Promise.all
which rejects fast and allows setBulkActionLoading(false) to run while other
mutations are still in-flight; change to Promise.allSettled over
selectedVideoList.map(video => operation(video)) so you wait for every mutation
to complete, then count results to build a success/failure summary (e.g.,
successes vs failures) and call showNotification with that summary; ensure
onSuccess is awaited/executed only after allSettled completes, still handle and
log any unexpected errors, and keep setBulkActionLoading(false) in finally so
the loading guard only clears after all operations have settled.
🧹 Nitpick comments (1)
frontend/app/components/videos/Grid.tsx (1)

186-188: No concurrency limit on bulk mutations.

Promise.all(selectedVideoList.map(…)) fires every mutation simultaneously. If a user selects videos across many pages, this could send hundreds of concurrent requests. Consider batching (e.g., chunks of 5–10) or using a concurrency-limited helper like p-limit to avoid overwhelming the backend.

@Zibbp Zibbp merged commit 32cafe1 into main Feb 8, 2026
7 of 8 checks passed
@Zibbp Zibbp deleted the bulk-video-actions branch February 8, 2026 16:07
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