Back to Source Monitor

Bulk Scrape Enablement with Confirmation Modal

.vbw-planning/milestones/ui-fixes-and-smart-scraping/phases/04-smart-scrape-recommendations/04-PLAN-05.md

0.13.08.2 KB
Original Source

Plan 05: Bulk Scrape Enablement with Confirmation Modal

Overview

Add the ability to select multiple scrape candidate sources and enable scraping for them in bulk. Includes checkboxes on source rows, a confirmation modal with count/warning, and a controller action that updates scraping_enabled on selected sources.


Task 1: Create BulkScrapeEnablementsController

Files:

  • app/controllers/source_monitor/bulk_scrape_enablements_controller.rb

Description: Create a controller for bulk-enabling scraping on selected sources. Follows CRUD-everything pattern as a standalone resource (not nested under individual source):

ruby
module SourceMonitor
  class BulkScrapeEnablementsController < ApplicationController
    def create
      source_ids = Array(params.dig(:bulk_scrape_enablement, :source_ids)).map(&:to_i).reject(&:zero?)

      if source_ids.empty?
        handle_empty_selection
        return
      end

      sources = Source.where(id: source_ids, scraping_enabled: false)
      updated_count = sources.update_all(
        scraping_enabled: true,
        scraper_adapter: default_adapter,
        updated_at: Time.current
      )

      respond_to do |format|
        format.turbo_stream do
          responder = SourceMonitor::TurboStreams::StreamResponder.new
          responder.toast(
            message: "Scraping enabled for #{updated_count} #{'source'.pluralize(updated_count)}.",
            level: :success
          )
          # Redirect to refresh the sources index
          responder.redirect(source_monitor.sources_path)
          render turbo_stream: responder.render(view_context)
        end
        format.html do
          redirect_to source_monitor.sources_path,
            notice: "Scraping enabled for #{updated_count} #{'source'.pluralize(updated_count)}."
        end
      end
    end

    private

    def default_adapter
      SourceMonitor.config.scrapers.default_adapter_name || "readability"
    end

    def handle_empty_selection
      respond_to do |format|
        format.turbo_stream do
          responder = SourceMonitor::TurboStreams::StreamResponder.new
          responder.toast(message: "No sources selected.", level: :warning)
          render turbo_stream: responder.render(view_context), status: :unprocessable_entity
        end
        format.html do
          redirect_to source_monitor.sources_path, alert: "No sources selected."
        end
      end
    end
  end
end

Tests:

  • test/controllers/source_monitor/bulk_scrape_enablements_controller_test.rb:
    • POST create with valid source_ids: updates sources, returns success toast
    • POST create with empty source_ids: returns warning
    • Only updates sources where scraping_enabled is false
    • Sets scraper_adapter to default adapter
    • Turbo stream format returns redirect action

Task 2: Add route for bulk scrape enablements

Files:

  • config/routes.rb

Description: Add top-level resource route (not nested under sources since it operates across multiple sources):

ruby
resources :bulk_scrape_enablements, only: :create

Tests:

  • Route test: assert POST /bulk_scrape_enablements routes to bulk_scrape_enablements#create

Task 3: Add checkboxes and bulk action bar to sources index

Files:

  • app/views/source_monitor/sources/index.html.erb
  • app/views/source_monitor/sources/_row.html.erb

Description: Wrap the sources table in a data-controller="select-all" scope. Add:

  1. Header checkbox in the table header (master select-all):
erb
<th scope="col" class="w-10 px-3 py-3">
  <input type="checkbox"
         data-select-all-target="master"
         data-action="select-all#toggleAll"
         class="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
         aria-label="Select all sources">
</th>
  1. Row checkboxes in _row.html.erb (only for scrape candidates):
erb
<td class="w-10 px-3 py-4">
  <% if scrape_candidates.include?(source.id) %>
    <input type="checkbox"
           name="bulk_scrape_enablement[source_ids][]"
           value="<%= source.id %>"
           data-select-all-target="item"
           data-action="select-all#toggleItem"
           class="rounded border-slate-300 text-violet-600 focus:ring-violet-500"
           aria-label="Select <%= source.name %>">
  <% end %>
</td>
  1. Bulk action bar below the table (sticky bottom bar, hidden when no checkboxes checked): Create a Stimulus controller bulk-action-bar that shows/hides based on checkbox state, or reuse select-all with a connected bar target. Add a form that submits to bulk_scrape_enablements#create with a confirmation modal:
erb
<div data-select-all-target="actionBar" class="hidden sticky bottom-0 border-t border-slate-200 bg-white px-4 py-3 shadow-md">
  <div class="flex items-center justify-between">
    <span class="text-sm text-slate-700">
      <span data-select-all-target="count">0</span> source(s) selected
    </span>
    <button type="button"
            data-action="modal#open"
            class="inline-flex items-center rounded-md bg-violet-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-violet-500">
      Enable Scraping
    </button>
  </div>
</div>

Tests:

  • System/integration test: checkboxes appear for candidate sources, not for non-candidates

Task 4: Create confirmation modal partial

Files:

  • app/views/source_monitor/sources/_bulk_scrape_enable_modal.html.erb

Description: Create a confirmation modal using the existing modal Stimulus controller. The modal shows a warning and submits the bulk enablement form:

erb
<div data-controller="modal" class="relative">
  <div data-modal-target="panel" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50" data-action="click->modal#backdrop">
    <div class="w-full max-w-md rounded-lg bg-white shadow-xl" data-action="click->modal#stop">
      <div class="border-b border-slate-200 px-6 py-4">
        <h3 class="text-lg font-semibold text-slate-900">Enable Scraping</h3>
      </div>
      <div class="px-6 py-4">
        <p class="text-sm text-slate-700">
          This will enable scraping for the selected sources using the default scraper adapter.
          Each source's items will be scraped on their next scheduled run.
        </p>
        <p class="mt-3 text-sm font-medium text-amber-700">
          This action will modify the selected sources' configuration.
        </p>
      </div>
      <div class="flex justify-end gap-3 border-t border-slate-200 px-6 py-4">
        <button type="button"
                data-action="modal#close"
                class="rounded-md border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">
          Cancel
        </button>
        <button type="submit"
                class="rounded-md bg-violet-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-violet-500">
          Confirm Enable
        </button>
      </div>
    </div>
  </div>
</div>

The modal is wired into the form wrapping the sources table so the submit button posts the checked source IDs.

Tests:

  • System test: modal appears when "Enable Scraping" button clicked, "Confirm Enable" submits form

Task 5: Extend select-all Stimulus controller for action bar visibility

Files:

  • app/assets/javascripts/source_monitor/controllers/select_all_controller.js

Description: Extend the existing select-all controller to support an optional actionBar target and count target. When any checkbox is checked, show the action bar and update the count:

Add to static targets: "actionBar", "count"

Add method updateActionBar():

javascript
updateActionBar() {
  if (!this.hasActionBarTarget) return;
  const checkedCount = this.itemTargets.filter(cb => cb.checked).length;
  if (this.hasCountTarget) {
    this.countTarget.textContent = checkedCount;
  }
  if (checkedCount > 0) {
    this.actionBarTarget.classList.remove("hidden");
  } else {
    this.actionBarTarget.classList.add("hidden");
  }
}

Call this.updateActionBar() at the end of toggleAll(), toggleItem(), syncMaster(), itemTargetConnected(), and itemTargetDisconnected().

Tests:

  • yarn build must succeed (ESLint check)
  • System test: action bar appears/disappears based on checkbox state