mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-04-30 20:01:00 +08:00
## 🔍 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
181 lines
5.1 KiB
Ruby
181 lines
5.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe "User activity drafts", type: :system do
|
|
fab!(:user)
|
|
let(:drafts_page) { PageObjects::Pages::UserActivityDrafts.new }
|
|
|
|
before { sign_in(user) }
|
|
|
|
describe "bulk selection functionality" do
|
|
before do
|
|
# Create some drafts for testing
|
|
Draft.set(
|
|
user,
|
|
"#{Draft::NEW_TOPIC}_1",
|
|
0,
|
|
{ title: "Draft topic 1", reply: "First draft content" }.to_json,
|
|
)
|
|
Draft.set(
|
|
user,
|
|
"#{Draft::NEW_TOPIC}_2",
|
|
0,
|
|
{ title: "Draft topic 2", reply: "Second draft content" }.to_json,
|
|
)
|
|
Draft.set(
|
|
user,
|
|
"#{Draft::NEW_TOPIC}_3",
|
|
0,
|
|
{ title: "Draft topic 3", reply: "Third draft content" }.to_json,
|
|
)
|
|
end
|
|
|
|
it "shows bulk select checkboxes for drafts" do
|
|
drafts_page.visit(user)
|
|
|
|
expect(drafts_page).to have_drafts(count: 3)
|
|
expect(drafts_page).to have_bulk_select_checkboxes
|
|
end
|
|
|
|
it "shows bulk controls only after selecting items" do
|
|
drafts_page.visit(user)
|
|
|
|
# Initially no bulk controls should be visible
|
|
expect(drafts_page).to have_no_bulk_controls
|
|
|
|
# Select a draft
|
|
drafts_page.select_draft(0)
|
|
|
|
# Now bulk controls should be visible
|
|
expect(drafts_page).to have_bulk_controls
|
|
expect(drafts_page).to have_selected_count(1)
|
|
end
|
|
|
|
it "updates selection count when selecting multiple drafts" do
|
|
drafts_page.visit(user)
|
|
|
|
# Select first draft
|
|
drafts_page.select_draft(0)
|
|
expect(drafts_page).to have_selected_count(1)
|
|
|
|
# Select second draft
|
|
drafts_page.select_draft(1)
|
|
expect(drafts_page).to have_selected_count(2)
|
|
|
|
# Select third draft
|
|
drafts_page.select_draft(2)
|
|
expect(drafts_page).to have_selected_count(3)
|
|
end
|
|
|
|
it "selects all drafts with select all button" do
|
|
drafts_page.visit(user)
|
|
|
|
# Select one draft to show controls
|
|
drafts_page.select_draft(0)
|
|
expect(drafts_page).to have_bulk_controls
|
|
|
|
# Click select all
|
|
drafts_page.click_bulk_select_all
|
|
expect(drafts_page).to have_selected_count(3)
|
|
|
|
# All checkboxes should be checked
|
|
expect(drafts_page).to have_checkbox_checked(0)
|
|
expect(drafts_page).to have_checkbox_checked(1)
|
|
expect(drafts_page).to have_checkbox_checked(2)
|
|
end
|
|
|
|
it "clears all selections with clear all button" do
|
|
drafts_page.visit(user)
|
|
|
|
# Select all drafts
|
|
drafts_page.select_draft(0)
|
|
drafts_page.select_all_drafts
|
|
|
|
# Clear all selections
|
|
drafts_page.clear_all_selections
|
|
|
|
# No checkboxes should be checked and controls should be hidden
|
|
expect(drafts_page).to have_checkbox_unchecked(0)
|
|
expect(drafts_page).to have_checkbox_unchecked(1)
|
|
expect(drafts_page).to have_checkbox_unchecked(2)
|
|
expect(drafts_page).to have_no_bulk_controls
|
|
end
|
|
|
|
it "shows selected styling on selected items" do
|
|
drafts_page.visit(user)
|
|
|
|
# Select first draft
|
|
drafts_page.select_draft(0)
|
|
|
|
# First item should have selected styling, second should not
|
|
expect(drafts_page).to have_checkbox_checked(0)
|
|
expect(drafts_page).to have_checkbox_unchecked(1)
|
|
end
|
|
|
|
it "toggles selection when clicking checkbox" do
|
|
drafts_page.visit(user)
|
|
|
|
# Initially unchecked
|
|
expect(drafts_page).to have_checkbox_unchecked(0)
|
|
|
|
# Click to select
|
|
drafts_page.select_draft(0)
|
|
expect(drafts_page).to have_checkbox_checked(0)
|
|
expect(drafts_page).to have_bulk_controls
|
|
|
|
# Click to deselect
|
|
drafts_page.select_draft(0)
|
|
expect(drafts_page).to have_checkbox_unchecked(0)
|
|
expect(drafts_page).to have_no_bulk_controls
|
|
end
|
|
|
|
it "bulk deletes selected drafts" do
|
|
drafts_page.visit(user)
|
|
|
|
# Select first two drafts
|
|
drafts_page.select_draft(0)
|
|
drafts_page.select_draft(1)
|
|
expect(drafts_page).to have_selected_count(2)
|
|
|
|
# Bulk delete
|
|
drafts_page.click_bulk_delete
|
|
|
|
# Confirm deletion in modal
|
|
page.find(".dialog-footer .btn-danger").click
|
|
|
|
# Should have only one draft left and no bulk controls
|
|
expect(drafts_page).to have_drafts(count: 1)
|
|
expect(drafts_page).to have_no_bulk_controls
|
|
expect(Draft.count).to eq(1)
|
|
end
|
|
|
|
it "clears selection after successful bulk delete" do
|
|
drafts_page.visit(user)
|
|
|
|
# Select all drafts
|
|
drafts_page.select_draft(0)
|
|
drafts_page.select_all_drafts
|
|
expect(drafts_page).to have_selected_count(3)
|
|
|
|
# Bulk delete all
|
|
drafts_page.click_bulk_delete
|
|
|
|
# Confirm deletion
|
|
page.find(".dialog-footer .btn-danger").click
|
|
|
|
# Should have no drafts and no controls
|
|
expect(drafts_page).to have_no_drafts
|
|
expect(drafts_page).to have_no_bulk_controls
|
|
expect(Draft.count).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe "without bulk selection" do
|
|
it "does not show bulk select controls when no drafts exist" do
|
|
drafts_page.visit(user)
|
|
|
|
expect(drafts_page).to have_no_drafts
|
|
expect(drafts_page).to have_no_bulk_select_checkboxes
|
|
expect(drafts_page).to have_no_bulk_controls
|
|
end
|
|
end
|
|
end
|