Skip to content

Add negation and JS-expression search in AE listing detail table - #146

Open
yihui wants to merge 7 commits into
mainfrom
feature/search-negation
Open

Add negation and JS-expression search in AE listing detail table#146
yihui wants to merge 7 commits into
mainfrom
feature/search-negation

Conversation

@yihui

@yihui yihui commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a custom searchMethod to the nested AE listing reactable so users can filter detail rows with either:
    • Substring search (default), with an optional leading ! to exclude matches — e.g. Rash keeps matching rows, !Rash hides them.
    • JS expression search when the term references the cell variable x — e.g. x > 50, x !== "Rash", x.includes("Rash"), !x.includes("Rash").
  • Expression mode uses some() for positive tests (row kept if ANY cell matches) and every() for negation tests (row kept only if EVERY cell satisfies, so rows containing the excluded value are dropped).
  • Substring vs expression mode is disambiguated by whether the term references x, so !Rash and x !== "Rash" no longer collide.

Testing

Verified in a headless browser (chromote) against a reactable using the exact searchMethod:

input result
!Rash rows without "Rash"
Rash rows with "Rash"
x !== "Rash" rows without "Rash"
x > 50 numeric filter
!x.includes("Rash") rows without "Rash"

devtools::test(filter = "ae_forestly") passes (30 checks), including a new regression test asserting the custom searchMethod is embedded.

Supersedes #134 (same branch, now hosted on Merck/forestly).

🤖 Generated with Claude Code

yihui and others added 3 commits June 10, 2026 10:28
Users can now prefix search terms with "!" to exclude matching rows
(e.g., "!group A" filters out rows containing "group A").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can now type arbitrary JS expressions in the search bar using `x`
as the cell value, e.g. `x != 'Placebo'`, `x > 80`, `x.includes('foo')`.
Negation expressions filter with every() (exclude matching), positive
expressions filter with some() (include matching). Plain text still works
as substring search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore the every()/some() split for JS-expression searches so that
negation expressions filter correctly:

- A negation expression (e.g. `x !== "Rash"`, `!x.includes("Rash")`) now
  keeps a row only when EVERY cell satisfies the test, so rows whose value
  matches the excluded term are dropped. Previously it used some(), so any
  non-matching cell (e.g. age) kept the whole row and nothing was filtered.
- Positive expressions still keep a row when ANY cell satisfies the test.

Also keeps the substring path with a leading `!` for simple negation
(e.g. `!Rash`), which is disambiguated from expression mode by whether the
term references the cell variable `x`.

Verified in a headless browser (chromote) against a reactable with the
exact searchMethod: `!Rash`, `Rash`, `x !== "Rash"`, `x > 50`, and
`!x.includes("Rash")` all filter as expected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yihui and others added 4 commits August 28, 2026 18:00
The previous change only customized the table-wide `searchMethod` (the
global search box), but the AE listing detail table exposes a filter box
per column (`filterable = TRUE`). Those per-column boxes use the default
substring `filterMethod`, so `!Rash` or `x !== "Rash"` matched nothing
and returned an empty table.

Add a custom `filterMethod` to every column definition mirroring the
global search behavior for a single column:

- Substring match, with a leading `!` to negate (e.g. `!Rash`).
- JS expression evaluation when the term references the cell value `x`
  (e.g. `x > 5`, `x !== "Rash"`, `!x.includes("Rash")`), tried against
  both the string and, when applicable, numeric form of the cell.

Verified in a headless browser (chromote) against the real ae_forestly()
widget: expanding a detail row and typing into the Adverse Event and
Gender column filters, `F`/`!F`, `x === "F"`/`x !== "F"`, and
`!x.includes(...)` all filter that column as expected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The main forest-plot table (built via reactable2() from
format_ae_forestly()) renders a per-column filter box for the Adverse
Event column. This is the column where excluding a term is actually
meaningful, since it lists every AE (Rash, Pruritus, Erythema, ...).

Previous commits added the custom filter/search behavior only to the
table-wide search box and to the nested detail table, so typing `!Rash`
or `x !== "Rash"` in the Adverse Event filter fell through to reactable's
default substring matcher: it searched for the literal text and returned
an empty table (even `x === "Rash"` matched nothing).

Attach a custom `filterMethod` to the `name` (Adverse Event) column
definition supporting:
- substring match, with a leading `!` to negate (e.g. `!Rash`), and
- JS expression evaluation when the term references the cell value `x`
  (e.g. `x !== "Rash"`, `!x.includes("site")`).

Verified in a headless browser (chromote) against the ae_forestly()
widget produced by the documented pipeline: `Rash` -> 5 AEs, `!Rash` and
`x !== "Rash"` -> all AEs, `x === "Rash"` -> 1, `!x.includes("site")`
-> all non-"Application site *" AEs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The negation/expression search logic was copy-pasted in three places:
the table-wide searchMethod and the per-column filterMethod of the nested
detail table (R/ae_forestly.R), and the Adverse Event column filter
(R/format_ae_forestly.R).

Extract a single internal helper, search_filter_js(scope), that emits the
reactable::JS() callback for either a per-column filterMethod
(scope = "column") or a table-wide searchMethod (scope = "table"). The
two share one body: the searched column ids are normalized to an array
(`[columnId]` vs `columnIds`), and some()/every() over that array yields
the single-cell behavior for a column filter and the any/all-cell
behavior for a table search.

No behavior change. Verified in a headless browser (chromote) that the
Adverse Event column filter and the nested detail table's column filter
and search still handle substring, `!` negation, and `x`-expression terms
identically. `devtools::test(filter = "ae_forestly")` passes (33 checks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a user-facing "Searching and filtering" section to ?ae_forestly
explaining the two search styles the interactive table understands beyond
plain substring matching:

- Negation: prefix a term with `!` to exclude matching rows (e.g. `!Rash`).
- Expressions: a term mentioning `x` (the cell value) is evaluated as a
  small condition, e.g. `x > 5`, `x >= 18 && x <= 65`, `x !== "Rash"`,
  `x.includes("itch")`, `x.startsWith("Application")`.

The section is written for R users who may not know JavaScript: it gives a
two-column table of ready-to-type examples with plain-English meanings and
notes on quoting, `&&`/`||`/`!`, the doubled comparison symbols, and
case-sensitivity. Every example in the table was verified against the
actual search implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yihui
yihui requested a review from LittleBeannie August 28, 2026 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant