Skip to content

feat(#6502): Filter for new case contact table#6942

Open
cliftonmcintosh wants to merge 3 commits intorubyforgood:mainfrom
cliftonmcintosh:feature/6502-case-contacts-filter
Open

feat(#6502): Filter for new case contact table#6942
cliftonmcintosh wants to merge 3 commits intorubyforgood:mainfrom
cliftonmcintosh:feature/6502-case-contacts-filter

Conversation

@cliftonmcintosh
Copy link
Copy Markdown
Collaborator

@cliftonmcintosh cliftonmcintosh commented May 8, 2026

What github issue is this PR for, if any?

Resolves #6502

What changed, and why?

Adds a filter button and collapsible filter panel to the new case contacts table (/case_contacts/new_design).

Backend

  • app/datatables/case_contact_datatable.rb

Added a new apply_additional_filters method that pipes the datatable query through existing CaseContact model scopes before returning results. Each scope already handles nil/blank values gracefully, so only active filters affect the query. Filters combine with AND logic — a record must satisfy every active filter simultaneously.

Filters supported:

  • Date range (occurred starting at / ending at)
  • Case (one or more case IDs)
  • Relationship / contact type (one or more contact type IDs)
  • Medium (single-select dropdown)
  • Contacted / contact made (single-select dropdown)
  • Hide drafts (checkbox)

Frontend

  • app/views/case_contacts/case_contacts_new_design/index.html.erb
  • app/javascript/src/dashboard.js

Restructured the page header into a filter toolbar row with the Filter button on the left and New Case Contact on the right.

  • Clicking Filter toggles a collapsible panel containing all filter inputs.
  • Apply Filters triggers a DataTables reload with the active filters attached to the request
  • Reset clears all inputs and reloads.

The filter values are collected by a collectCaseContactFilters() function and attached to every DataTables POST request via the ajax.data hook, so filters remain in effect when the user sorts or pages through results.

How is this tested? (please write rspec and jest tests!) 💖💪

Request specs (spec/requests/case_contacts/case_contacts_new_design_spec.rb)

Nine tests covering each filter param individually and in combination:

  • occurred_starting_at — returns only contacts on or after the date
  • occurred_ending_at — returns only contacts on or before the date
  • casa_case_ids — returns only contacts for the specified cases
  • contact_type_ids — returns only contacts with the specified contact types
  • contact_medium — returns only contacts with the specified medium
  • contact_made: true — returns only contacts where contact was made
  • contact_made: false — returns only contacts where contact was not made
  • no_drafts — excludes draft contacts
  • Multiple filters combined — all active filters apply together (AND)

System specs (spec/system/case_contacts/case_contacts_new_design_spec.rb)

Seven tests covering the filter UI:

  • Filter button is visible on the page
  • Filter panel is hidden by default
  • Clicking Filter opens the panel; clicking again closes it
  • Applying a medium filter shows only contacts with that medium
  • Applying a date range filter shows only contacts within the range
  • Checking Hide Drafts excludes draft rows
  • Reset clears all filters and shows all contacts again

Screen recordings

Opening and closing the filter panel

The Filter button lives in the toolbar row to the left of the New Case Contact button. Clicking it toggles a collapsible panel containing all filter inputs. The panel is hidden by default and does not affect the table until a filter is applied.

Screen.Recording.2026-05-08.at.11.15.50.mov
Applying a single filter

Selecting a value and clicking Apply Filters triggers a DataTables reload with the filter attached to the server request. Only rows matching the filter are returned; the table updates in place without a full page refresh.

Screen.Recording.2026-05-08.at.11.16.39.mov
Combining multiple filters and resetting

All active filters are applied together with AND logic — a contact must satisfy every active filter to appear in the results. Clicking Reset clears all inputs and reloads the table with no filters applied, restoring the full result set.

Screen.Recording.2026-05-08.at.11.20.59.mov
Mobile view

The filter toolbar and panel are built with Bootstrap's responsive grid. At phone-sized widths:

  • The toolbar buttons wrap to separate lines rather than overflowing
  • Each filter section (date inputs, case checkboxes, relationship checkboxes, dropdowns) stacks to full width
  • All inputs remain usable without horizontal scrolling
Screen.Recording.2026-05-08.at.11.13.41.mov

Differences from the Figma design

The Figma mockup informed this implementation but was not followed exactly. Questions were raised with the designer via this Slack thread but went unanswered. A fellow developer advised:

Since we haven't had any feedback from them in a couple weeks, I think you can make your best guess as to what should be done. We can always improve it later.

The deviations are intentional and documented below.

Flat panel instead of drill-down overlay

The Figma shows a two-level drill-down overlay: clicking Filter opens a category list, and clicking a category slides into a sub-panel with that filter's options. A single flat panel was implemented instead, showing all filters at once. The flat panel is simpler to build, simpler to test, and easier to reason about. The backend additional_filters param contract is identical regardless of UI shape, so replacing the flat panel with the drill-down overlay in a future ticket is a pure frontend change with no backend work required.

Medium, Contacted, and Draft — single-select instead of multi-select

The Figma shows these three as checkbox-based filters supporting multiple simultaneous selections. All three were implemented as single-select dropdowns instead. The existing model scopes for these filters accept a single value; supporting multi-select would require updating the scopes and the frontend. The single-select versions deliver useful functionality now, and the upgrade path is straightforward when prioritised.

Created By and Topics filters omitted

The Figma includes Created By and Topics filters. Both were skipped. Created By has an existing backend scope but raises an open question about which users to list in the UI. Topics has no backend scope at all. Both are self-contained additions that can be done in follow-up tickets without changing anything built here.

Want Driving Reimbursement filter dropped

The old design included a Want Driving Reimbursement filter. The Figma mockup for the new design does not include it, so it was omitted.

Feelings gif (optional)

filtering like a pro

cliftonmcintosh and others added 2 commits May 8, 2026 10:24
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the filter toolbar (Filter button + New Case Contact), a
collapsible filter panel (date range, case, relationship, medium,
contacted, hide drafts), and the JS to collect and POST
additional_filters on apply/reset. Includes system specs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added 🧪 Tests Tests javascript Touches JavaScript code ruby Touches Ruby code erb Touches ERB templates labels May 8, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cliftonmcintosh cliftonmcintosh marked this pull request as ready for review May 8, 2026 16:25
@cliftonmcintosh cliftonmcintosh self-assigned this May 8, 2026
Comment on lines +61 to 70
def apply_additional_filters(records)
records = records.occurred_starting_at(additional_filters[:occurred_starting_at])
records = records.occurred_ending_at(additional_filters[:occurred_ending_at])
records = records.with_casa_case(Array(additional_filters[:casa_case_ids])) if additional_filters[:casa_case_ids].present?
records = records.contact_type(Array(additional_filters[:contact_type_ids])) if additional_filters[:contact_type_ids].present?
records = records.contact_medium(additional_filters[:contact_medium])
records = records.contact_made(additional_filters[:contact_made])
records = records.no_drafts(additional_filters[:no_drafts].to_i) if additional_filters[:no_drafts].present?
records
end
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new method rather than extending search_filter because the two are fundamentally different in structure. search_filter builds a raw SQL WHERE clause that ORs conditions together against a single free-text string — merging AND-logic filter conditions into it would break both features. apply_additional_filters chains named ActiveRecord scopes instead, each adding an ANDed WHERE condition, which is exactly the semantics filters need.

<% end %>
</div>

<div id="cc-filter-panel" class="card-style mb-3" style="display: none;">
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline style rather than a CSS class is intentional here. The inline style takes effect immediately as the browser parses the HTML, before any JavaScript loads. A CSS class would leave a window where the panel is briefly visible while the JS bundle is still loading, causing a flash of unwanted content on page load.

if (casaIds.length > 0) filters.casa_case_ids = casaIds
if (contactTypeIds.length > 0) filters.contact_type_ids = contactTypeIds
if (medium) filters.contact_medium = medium
if (contactMade !== '') filters.contact_made = contactMade
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!== '' rather than a plain truthy check is intentional. The "Not Reached" option has the value "false", which is a non-empty string but would fail a truthy check, silently dropping the filter. The empty string "" is the only value that means "no filter selected" (the default "Display all" option), so checking against that specifically is a deliberate choice.

recent_date = I18n.l(video_contact.occurred_at, format: :full)

click_button "Filter"
execute_script("document.getElementById('cc-filter-occurred-ending-at').value = '#{4.days.ago.to_date}'")
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute_script is used here instead of Capybara's fill_in or find.set because those can be unreliable for type="date" inputs in headless Chrome — the browser's native date picker intercepts the interaction and jQuery does not see the resulting value. Setting the value directly on the DOM element via JavaScript bypasses the picker and gives jQuery a value it can read with .val() when the filter is applied.

Comment on lines +4 to +8
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3" id="cc-filter-toolbar">
<button type="button" id="cc-filter-toggle" class="main-btn secondary-btn btn-sm btn-hover">
<i class="lni lni-funnel mr-5" aria-hidden="true"></i>
Filter
</button>
Copy link
Copy Markdown
Collaborator Author

@cliftonmcintosh cliftonmcintosh May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the color of this button be the same as the color of the New Case Contact button? The Figma has it as white with a dark blue icon and dark blue text.

As coded:

Image

In the Figma:

Image

Here's my rationale for the current color:

"New Case Contact" is the primary action on the page. The Filter button is a utility action, used less frequently and by a subset of users. Giving the primary action a filled, prominent style and the secondary action a less prominent style guides the eye to the most important action without hiding the secondary one.

If they were the same colour, the two buttons would compete for attention equally, which would be visually noisy and would undermine the distinction between "the thing you're here to do" and "a tool to help you find things."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is what it would look like with the dark-btn-outline version. I think this makes it hard to see until hover.

Screen.Recording.2026-05-08.at.13.26.41.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

erb Touches ERB templates javascript Touches JavaScript code ruby Touches Ruby code 🧪 Tests Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New Case Contact Table: implement "filter" button/functionality

1 participant