.vbw-planning/milestones/07-rails-audit-and-refactoring/05-view-layer-extraction/02-PLAN.md
Extract duplicated filter dropdown markup from 3 index views into a shared FilterDropdownComponent (V5), extract filter state logic from sources/index.html.erb into a SourcesFilterPresenter (V1), document row preload requirements (V3), and standardize Turbo Frame naming (V12).
app/views/source_monitor/sources/index.html.erb (282 lines) -- has 56 lines of filter logic (V1) and repeated filter selects (V5)app/views/source_monitor/items/index.html.erb -- same filter dropdown patternapp/views/source_monitor/logs/index.html.erb -- same filter dropdown patternapp/views/source_monitor/sources/_row.html.erb -- undocumented preload requirements (V3)Create test/components/source_monitor/filter_dropdown_component_test.rb:
Create app/components/source_monitor/filter_dropdown_component.rb:
initialize(label:, param_name:, options:, selected_value: nil, form: nil)call method renders label + select with Tailwind stylingonchange attributeCreate app/components/source_monitor/filter_dropdown_component.html.erb (or use inline call).
Note: If ViewComponent gem is not yet in the gemspec, add view_component to source_monitor.gemspec as a runtime dependency. Create app/components/source_monitor/application_component.rb base class.
Create test/presenters/source_monitor/sources_filter_presenter_test.rb:
has_any_filter? returns false when no filters activehas_any_filter? returns true when search term presenthas_any_filter? returns true when dropdown filter activeactive_filter_keys returns only keys with present valuesfilter_labels returns hash with humanized labels for active filtersadapter_options returns distinct scraper adapter valuesCreate app/presenters/source_monitor/sources_filter_presenter.rb:
initialize(search_params:, search_term:, fetch_interval_filter:, adapter_options:)has_any_filter?, active_filter_keys, filter_labels, clear_filter_path(key, current_params)Modify app/views/source_monitor/sources/index.html.erb:
render SourceMonitor::FilterDropdownComponent.new(...)@filter_presenter method callsadapter_options query from template to controller (assign to presenter)import_session_step frame ID is renamed to source_monitor_import_step if present (V12)Modify app/controllers/source_monitor/sources_controller.rb (index action only):
@filter_presenter = SourceMonitor::SourcesFilterPresenter.new(...) with search paramsModify app/views/source_monitor/items/index.html.erb:
render SourceMonitor::FilterDropdownComponent.new(...)Modify app/views/source_monitor/logs/index.html.erb:
render SourceMonitor::FilterDropdownComponent.new(...)bin/rails test test/components/ -- all component tests passbin/rails test test/presenters/source_monitor/sources_filter_presenter_test.rb -- all passbin/rails test test/controllers/ -- all controller tests passbin/rails test -- full suite passesbin/rubocop -- zero offenses