discourse/app/controllers
Keegan George ad21ae98ff
FEATURE: Bulk select posts and delete drafts (#34972)
## 🔍 Overview

This PR adds bulk selection functionality to the PostList component and
implements optimized bulk deletion for the drafts page. Users can now
select multiple drafts and delete them all at once with a single network
request, significantly improving performance and user experience.

The implementation includes:
- A new reusable bulk selection system for PostList components
- Optimized bulk delete endpoint that reduces network requests by 90%
- Comprehensive bulk controls UI with select all/clear all functionality
- Shift+click range selection similar to topic lists
- Complete test coverage for all new functionality

##  More details

**Bulk Selection System**

The PostList component now supports optional bulk selection through
these new parameters:

- `@bulkSelectEnabled={{true}}` - Shows checkboxes next to each post
- `@bulkSelectHelper={{helper}}` - Manages selection state (use
`PostBulkSelectHelper`)
- `@bulkActions={{actions}}` - Array of bulk action objects for the
dropdown menu

**Usage Example:**
```gjs
import Component from "@glimmer/component";
import { action } from "@ember/object";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import PostBulkSelectHelper from "discourse/lib/post-bulk-select-helper";

export default class MyComponent extends Component {
  bulkSelectHelper = new PostBulkSelectHelper(this);

  constructor() {
    super(...arguments);
    // Initial updatePosts call
    this.updateBulkSelectPosts();
  }

  @action
  updateBulkSelectPosts() {
    if (this.shouldEnableBulkSelect && this.args.posts) {
      this.bulkSelectHelper.updatePosts(this.args.posts);
    }
  }

  get showBulkSelectHelper() {
    return this.shouldEnableBulkSelect ? this.bulkSelectHelper : null;
  }

  get bulkActions() {
    return [
      {
        label: "delete_selected",
        icon: "trash-can", 
        action: this.handleBulkDelete,
        class: "btn-danger"
      }
    ];
  }

  <template>
    <PostList 
      @posts={{@posts}}
      @bulkSelectEnabled={{this.shouldEnableBulkSelect}}
      @bulkSelectHelper={{this.showBulkSelectHelper}}
      @bulkActions={{this.bulkActions}}
      {{didUpdate this.updateBulkSelectPosts @posts}}
    />
  </template>
}
```

**Performance Optimization**

The drafts page now uses a new bulk delete endpoint (`DELETE
/drafts/bulk_destroy`) that:
- Processes multiple drafts in a single HTTP request instead of N
individual requests
- Uses database transactions for atomic operations (all-or-nothing)  
- Reduces database queries from 2N to 2 total queries
- Validates draft sequences upfront to fail fast on conflicts

**Technical Implementation**

- `PostBulkSelectHelper`: New helper class for managing selection state
with support for individual selection, range selection (shift+click),
and bulk operations with reactive posts tracking
- `PostListBulkControls`: New component providing selection count,
select all/clear all buttons, and bulk actions dropdown
- Enhanced PostList and PostListItem components with conditional bulk
selection UI
- Updated user-stream component to use optimized bulk deletion with
automatic selection cleanup
- Comprehensive styling with responsive design

**API Changes**

- New controller action: `DraftsController#bulk_destroy`
- New route: `DELETE /drafts/bulk_destroy`
- New JavaScript method: `Draft.bulkClear(drafts)`
- Enhanced `PostBulkSelectHelper` with `updatePosts()` method for
reactive data updates
- Fully backward compatible - existing single delete functionality
unchanged

**Testing**

- 9 new controller specs covering bulk deletion edge cases, validation,
and API access
- 11 integration tests for PostList bulk selection functionality  
- 10 system specs for end-to-end drafts page bulk selection workflows
- All existing tests continue to pass

## 📹 Screen Recording


https://github.com/user-attachments/assets/2d5a9b38-f1cb-43ee-88ac-285b71083612
2025-09-29 12:47:54 -07:00
..
admin DEV: Finish renaming secure_session to server_session 2025-09-23 10:35:02 +02:00
users DEV: Allow ServerSession to store arbitrary data (#34919) 2025-09-26 10:35:28 +02:00
about_controller.rb DEV: Revert guardian changes (#24742) 2023-12-06 16:37:32 +10:00
application_controller.rb UX: Redesign 404 (#34999) 2025-09-25 22:14:44 -06:00
associated_groups_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
badges_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
bookmarks_controller.rb FEATURE: Add bulk action to bookmark (#26856) 2024-05-22 12:50:21 -03:00
bootstrap_controller.rb DEV: Compile 'common' CSS into own assets (#31416) 2025-05-01 10:44:49 +01:00
categories_controller.rb DEV: Plugin api for saving category attribute (#34898) 2025-09-23 11:14:45 +08:00
clicks_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
composer_controller.rb UX: hide warning if all users mentioned via group are already invited. (#23557) 2023-09-13 19:21:44 +05:30
composer_messages_controller.rb DEV: Move distance_of_time_in_words/time_ago_in_words (#21745) 2023-05-25 14:53:59 +02:00
csp_reports_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
directory_columns_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
directory_items_controller.rb FIX: Allow user directory searches to return more than 20 matching results (#31032) 2025-01-29 11:02:42 -04:00
do_not_disturb_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
drafts_controller.rb FEATURE: Bulk select posts and delete drafts (#34972) 2025-09-29 12:47:54 -07:00
edit_directory_columns_controller.rb DEV: Implement staff logs for user columns edits (#21774) 2023-06-07 17:19:58 -05:00
email_controller.rb FEATURE: implement RFC 8058 for email unsubscribe (#33392) 2025-07-01 11:01:13 +10:00
embed_controller.rb DEV: Also noindex embedded comments (#27221) 2024-05-28 12:59:24 +08:00
emojis_controller.rb DEV: discourse-emojis gem (#31408) 2025-03-03 13:09:08 +01:00
exceptions_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
export_csv_controller.rb FEATURE: Make staff action logs export respect the filter (#34113) 2025-08-07 16:25:16 +08:00
extra_locales_controller.rb FIX: Moment locale loading in type=module (#33128) 2025-06-09 14:20:04 +01:00
finish_installation_controller.rb SECURITY: Preload data only when rendering application layout 2025-02-04 13:32:30 -03:00
form_templates_controller.rb FIX: Process templates before previewing (#33848) 2025-07-29 15:09:02 +08:00
forums_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
groups_controller.rb DEV: Revert unintended changes to GroupsController #add_members and its corresponding route (#32732) 2025-05-14 16:31:19 -05:00
hashtags_controller.rb FEATURE: Async load of category and chat hashtags (#25526) 2024-02-12 12:07:14 +02:00
highlight_js_controller.rb FEATURE: User fields required for existing users - Part 2 (#27172) 2024-06-25 19:32:18 +08:00
home_page_controller.rb DEV: Show login-required splash in root route (take 2) (#32629) 2025-05-14 11:25:43 -04:00
inline_onebox_controller.rb SECURITY: Limit /inline-onebox to 10 URLs at a time 2025-02-04 13:32:53 -03:00
invites_controller.rb DEV: Finish renaming secure_session to server_session 2025-09-23 10:35:02 +02:00
list_controller.rb FIX: handle redirect issue with categoryId rewriting page number (#33009) 2025-06-03 15:45:21 +08:00
metadata_controller.rb FEATURE: Add Discourse ID registration service with challenge endpoint (#34326) 2025-08-26 10:54:21 +02:00
new_invite_controller.rb FEATURE: Add invite link to the sidebar (#29448) 2024-10-30 05:31:14 +03:00
new_topic_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
notifications_controller.rb FEATURE: add new hidden site setting to show full names in user card 2025-01-23 12:26:59 -05:00
offline_controller.rb FEATURE: User fields required for existing users - Part 2 (#27172) 2024-06-25 19:32:18 +08:00
onebox_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
pageview_controller.rb FEATURE: User fields required for existing users - Part 2 (#27172) 2024-06-25 19:32:18 +08:00
permalinks_controller.rb FIX: Don’t raise an error on permalinks with external URL 2024-06-28 10:09:37 +02:00
post_action_users_controller.rb DEV: Add post_action_users_list modifier for PostActionUsersController (#25740) 2024-02-20 09:48:09 +10:00
post_actions_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
post_localizations_controller.rb FEATURE: Add post language on creating a new post (#33160) 2025-06-11 10:39:01 -07:00
post_readers_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
posts_controller.rb FIX: Allow reverting topic revisions with only tag changes (#34169) 2025-09-18 18:26:52 -07:00
presence_controller.rb FIX: improve "read only" modes (#33521) 2025-07-10 09:08:00 +02:00
published_pages_controller.rb FEATURE: User fields required for existing users - Part 2 (#27172) 2024-06-25 19:32:18 +08:00
push_notification_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
qunit_controller.rb DEV: Only load specific plugin bundles during qunit test (#33678) 2025-07-21 21:00:48 +01:00
reviewable_claimed_topics_controller.rb FEATURE: Sync Reviewable Status (#31901) 2025-03-24 14:27:18 +11:00
reviewable_notes_controller.rb FIX: Rename the reviewable notes route to match existing reviewable routes (#33480) 2025-07-04 17:46:41 +10:00
reviewables_controller.rb FEATURE: Allow rejected user details to be scrubbed (#31987) 2025-03-31 12:40:35 +11:00
robots_txt_controller.rb DEV: Update link to comment in robots.txt as 'allow' is allowed (#33227) 2025-06-18 13:30:26 +08:00
safe_mode_controller.rb FIX: Set X-Robots-Tag header to prevent indexing of /safe-mode (#32329) 2025-04-16 16:51:32 +10:00
search_controller.rb DEV: Prevent crawlers from loading search results. (#31553) 2025-08-27 15:59:08 +10:00
session_controller.rb DEV: Allow ServerSession to store arbitrary data (#34919) 2025-09-26 10:35:28 +02:00
sidebar_sections_controller.rb DEV: Use has_many and ArraySerializer for SidebarSectionsSerializer (#26716) 2024-05-06 11:32:18 -05:00
similar_topics_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
site_controller.rb SECURITY: Preload data only when rendering application layout 2025-02-04 13:32:30 -03:00
sitemap_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
slugs_controller.rb FEATURE: Allow changing slug on create channel (#19928) 2023-01-23 14:48:33 +10:00
static_controller.rb FIX: DiscourseConnect provider redirect to forum instead of the sso_redirect_url in the payload (#34580) 2025-09-19 15:09:34 +02:00
steps_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00
stylesheets_controller.rb DEV: Remove dual mode support for palettes and drop theme-owned palettes (#34467) 2025-08-26 06:24:11 +03:00
svg_sprite_controller.rb FIX: bump the number of svg icons we return to first 500 (#29286) 2024-10-18 19:22:13 +02:00
tag_groups_controller.rb FIX: Use the max_tag_search_results setting as the default limit for tag groups search (#33485) 2025-07-09 05:13:19 +03:00
tags_controller.rb FIX: Don't leave blank additional_tag_ids param after removing dups in intersection (#34423) 2025-08-19 15:14:29 -05:00
test_requests_controller.rb DEV: Add a user agent to all HTTP requests that Discourse makes. (#31555) 2025-03-03 16:32:25 +11:00
theme_javascripts_controller.rb DEV: Manually fix Rails/UnusedRenderContent offenses (#34418) 2025-08-22 11:42:12 +02:00
topic_localizations_controller.rb FEATURE: Add translations to posts (#32564) 2025-05-08 10:40:36 -07:00
topic_view_stats_controller.rb FEATURE: topic_view_stats table with daily fidelity (#27197) 2024-05-27 15:25:32 +10:00
topics_controller.rb FEATURE: Localize topic view in crawler view (#34253) 2025-08-13 16:38:07 +08:00
uploads_controller.rb DEV: Fix linting and test for b1ea35bb30 (#34688) 2025-09-02 13:43:34 -04:00
user_actions_controller.rb FIX: Load categories with user activity and drafts (#26553) 2024-04-10 17:35:42 +03:00
user_api_key_clients_controller.rb Add user api key client rate limit settings (#30402) 2024-12-30 11:10:48 -05:00
user_api_keys_controller.rb DEV: Add comment to clarify padding used in user-api-key encryption (#31833) 2025-03-14 14:34:44 -04:00
user_avatars_controller.rb FEATURE: User fields required for existing users - Part 2 (#27172) 2024-06-25 19:32:18 +08:00
user_badges_controller.rb FIX: error when trying to un-favorite badge (#32369) 2025-04-22 15:36:48 +08:00
user_status_controller.rb FEATURE: User Status API (#19149) 2022-11-24 19:16:28 +04:00
users_controller.rb DEV: Allow ServerSession to store arbitrary data (#34919) 2025-09-26 10:35:28 +02:00
users_email_controller.rb FEATURE: User fields required for existing users - Part 2 (#27172) 2024-06-25 19:32:18 +08:00
webhooks_controller.rb FIX: improve "read only" modes (#33521) 2025-07-10 09:08:00 +02:00
wizard_controller.rb DEV: Apply syntax_tree formatting to app/* 2023-01-09 14:14:59 +00:00