discourse/config
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
..
environments DEV: Migrate from sprockets to propshaft for assets (#32475) 2025-04-30 08:59:32 +01:00
initializers DEV: Improve system theme loading (#34954) 2025-09-24 17:56:41 +01:00
locales FEATURE: Bulk select posts and delete drafts (#34972) 2025-09-29 12:47:54 -07:00
application.rb DEV: Move debugging gem from byebug to debug (#34827) 2025-09-17 10:08:52 -03:00
boot.rb PERF: Stop running bootsnap in development mode on all environments (#25737) 2024-02-19 11:33:52 +08:00
cdn.yml.sample
database.yml DEV: Increase pool connections to 2 in test environment 2024-11-28 12:23:25 +01:00
deploy.rb.sample
dev_defaults.yml DEV: Convert admin-incoming-email modal to component-based API (#22701) 2023-07-20 16:31:20 -05:00
discourse.config.sample
discourse.pill.sample
discourse_defaults.conf DEV: Support connecting to Redis with a username. (#31710) 2025-03-13 10:39:50 +08:00
environment.rb DEV: Apply syntax_tree formatting to config/* 2023-01-09 11:13:29 +00:00
logrotate.conf
multisite.yml.production-sample
nginx.sample.conf PERF: Enable brotli in NGINX (#32333) 2025-04-24 15:45:13 +01:00
projections.json
puma.rb DEV: Apply syntax_tree formatting to config/* 2023-01-09 11:13:29 +00:00
routes.rb FEATURE: Bulk select posts and delete drafts (#34972) 2025-09-29 12:47:54 -07:00
sidekiq.yml
site_settings.yml UX: Enable 'viewport based mobile mode' by default (#35036) 2025-09-29 19:46:52 +01:00
spring.rb DEV: Apply syntax_tree formatting to config/* 2023-01-09 11:13:29 +00:00
thin.yml.sample
unicorn.conf.rb DEV: Fix undefined method check_email_sync_heartbeat in unicorn conf (#30360) 2024-12-19 10:10:11 +08:00
unicorn_launcher
unicorn_upstart.conf