From 92070a3be983ca9886b12584a9a54a55170150a9 Mon Sep 17 00:00:00 2001 From: Yihui Xie Date: Wed, 10 Jun 2026 10:28:23 -0400 Subject: [PATCH 1/2] Add negation search support in AE listing detail table 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 --- R/ae_forestly.R | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/R/ae_forestly.R b/R/ae_forestly.R index 6477686..b9d30a6 100644 --- a/R/ae_forestly.R +++ b/R/ae_forestly.R @@ -279,6 +279,19 @@ ae_forestly <- function(outdata, resizable = TRUE, filterable = TRUE, searchable = TRUE, + searchMethod = reactable::JS( + "function(rows, columnIds, filterValue) { + var negate = filterValue.startsWith('!'); + var term = negate ? filterValue.slice(1).trim() : filterValue.trim(); + if (term === '') return rows; + return rows.filter(function(row) { + var match = columnIds.some(function(id) { + return String(row.values[id]).toLowerCase().indexOf(term.toLowerCase()) > -1; + }); + return negate ? !match : match; + }); + }" + ), showPageSizeOptions = TRUE, borderless = TRUE, striped = TRUE, From 8a416a5529483ac03c55a540072ae508b8981d11 Mon Sep 17 00:00:00 2001 From: Yihui Xie Date: Wed, 10 Jun 2026 11:20:45 -0400 Subject: [PATCH 2/2] Support JS expressions in AE listing search bar 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 --- R/ae_forestly.R | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/R/ae_forestly.R b/R/ae_forestly.R index b9d30a6..276200a 100644 --- a/R/ae_forestly.R +++ b/R/ae_forestly.R @@ -281,14 +281,32 @@ ae_forestly <- function(outdata, searchable = TRUE, searchMethod = reactable::JS( "function(rows, columnIds, filterValue) { - var negate = filterValue.startsWith('!'); - var term = negate ? filterValue.slice(1).trim() : filterValue.trim(); - if (term === '') return rows; + var v = filterValue.trim(); + if (v === '') return rows; + // If the input looks like a JS expression (contains an operator), + // evaluate it with `x` bound to each cell value. + var exprPattern = /[=!<>]|\\.(includes|startsWith|endsWith|match)\\s*\\(/; + if (exprPattern.test(v)) { + try { + var fn = new Function('x', 'try { return (' + v + '); } catch(e) { return false; }'); + // Negation expressions use every() (row passes if ALL cells satisfy), + // positive expressions use some() (row passes if ANY cell satisfies). + var isNegation = /^!|!=/.test(v); + var method = isNegation ? 'every' : 'some'; + return rows.filter(function(row) { + return columnIds[method](function(id) { + var x = row.values[id]; + if (x == null) return isNegation; + return fn(String(x)) || (isFinite(Number(x)) && fn(Number(x))); + }); + }); + } catch(e) { } + } + // Default: substring search return rows.filter(function(row) { - var match = columnIds.some(function(id) { - return String(row.values[id]).toLowerCase().indexOf(term.toLowerCase()) > -1; + return columnIds.some(function(id) { + return String(row.values[id]).toLowerCase().indexOf(v.toLowerCase()) > -1; }); - return negate ? !match : match; }); }" ),