Skip to content

Commit

Permalink
Add server-side processing support for SearchBuilder (rstudio#1113)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikmart authored and AhmedKhaled945 committed Feb 2, 2024
1 parent b8a0032 commit ce84fce
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 0 deletions.
44 changes: 44 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
# CHANGES IN DT VERSION 0.32

- Fixed the bug that `replaceData()` failed to work with data that has no column names (thanks, @mmuurr, #1108).

- `updateSearch()` now sets the slider values based on the new search string for numeric columns (thanks, @mikmart, #1110).

- Added server-side processing support for the [SearchBuilder](https://datatables.net/extensions/searchbuilder/) extension (thanks, @AhmedKhaled945, @shrektan, @mikmart, #963).

# CHANGES IN DT VERSION 0.31

- Upgraded DataTables version to 1.13.6 (thanks, @stla, #1091).

- Searching and sorting work now when columns are re-ordered by the `ColReorder` extension (thanks, @ashbaldry #1096, @gergness #534, @nmoraesmunter #921, @isthisthat #1069).

- Fixed disabling selection on hyperlink clicks (thanks, @guoci, #1093).

- Fixed an error for R >= 4.3.0 (thanks, @AntoineMichelet, #1095).

# CHANGES IN DT VERSION 0.30

- Fixed a bug that when using `updateSearch()`, the clear button inside the input box doesn't show up, and the table doesn't update when the input is cleared (thanks, @DavidBlairs, #1082).

- Added support for a list of Booleans as input to the `class` argument of `DT::datatable()` when `style = 'bootstrap'` (thanks, @pedropark99, #1089). In other words, you can now select the Bootstrap classes you want to use at `DT::datatable()` by using a list of Booleans that select the classes you want to use. In the example below, we are producing an HTML table that uses the `stripe` and `hover` Bootstrap classes:

```r
DT::datatable(mtcars, class = list(stripe = TRUE, compact = FALSE, hover = TRUE), style = "bootstrap")
```

- Handle `NULL` return from `bslib::theme_version()` (thanks, @slodge-work, #1090).

# CHANGES IN DT VERSION 0.29

- Support Bootstrap 5 with `datatable(style = "auto")` (thanks, @gadenbuie, #1074).

- Fixed a bug that searching would fail when the keyword contains special characters such as `&` (thanks, @dfriend21 @olivier7121, #1075).

- Deleted `()` after `eval` in a JS *comment* to prevent Google from treating it as malware, which is just a false alarm (thanks, @gorkang, #1080).

# CHANGES IN DT VERSION 0.28

- Upgraded DataTables to v1.13.4 (thanks, @stla, #1063).

- It is now possible to edit date columns with the help of calendar inputs (thanks, @stla, #1064).

# CHANGES IN DT VERSION 0.27

- `dataTableOutput()` gains a new `fill` parameter. When `TRUE` (the default), the widget's container element is allowed to grow/shrink to fit it's parent container so long as that parent is opinionated about its height and has been marked with `htmltools::bindFillRole(x, container = TRUE)`. (#2198)
Expand Down
98 changes: 98 additions & 0 deletions R/searchbuilder.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# server-side processing for the SearchBuilder extension
# https://datatables.net/extensions/searchbuilder/

sbEvaluateSearch = function(search, data) {
# https://datatables.net/reference/option/searchBuilder.preDefined
stopifnot(search$logic %in% c('AND', 'OR'))
Reduce(
switch(search$logic, AND = `&`, OR = `|`),
lapply(search$criteria, sbEvaluateCriteria, data)
)
}

sbEvaluateCriteria = function(criteria, data) {
# https://datatables.net/reference/option/searchBuilder.preDefined.criteria
if ('logic' %in% names(criteria)) {
# this is a sub-group
sbEvaluateSearch(criteria, data)
} else {
# this is a criteria
cond = criteria$condition
type = criteria$type
x = data[[criteria$origData %||% criteria$data]]
v = sbParseValue(sbExtractValue(criteria), type)
sbEvaluateCondition(cond, type, x, v)
}
}

sbExtractValue = function(criteria) {
if (criteria$condition %in% c('between', '!between')) {
# array values are passed in a funny way to R
c(criteria$value1, criteria$value2)
} else {
criteria$value
}
}

sbParseValue = function(value, type) {
# TODO: handle 'moment' and 'luxon' types mentioned in condition reference
if (type %in% c('string', 'html')) {
as.character(value)
} else if (type %in% c('num', 'num-fmt', 'html-num', 'html-num-fmt')) {
as.numeric(value)
} else if (type %in% c('date')) {
as.Date(value)
} else {
stop(sprintf('unsupported criteria type "%s"', type))
}
}

sbEvaluateCondition = function(condition, type, x, value) {
# https://datatables.net/reference/option/searchBuilder.preDefined.criteria.condition
if (type %in% c('string', 'html')) {
switch(
condition,
'!=' = x != value,
'!null' = !is.na(x) & x != '',
'=' = x == value,
'contains' = grepl(value, x, fixed = TRUE),
'!contains' = !grepl(value, x, fixed = TRUE),
'ends' = endsWith(as.character(x), value),
'!ends' = !endsWith(as.character(x), value),
'null' = is.na(x) | x == '',
'starts' = startsWith(as.character(x), value),
'!starts' = !startsWith(as.character(x), value),
stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type))
)
} else if (type %in% c('num', 'num-fmt', 'html-num', 'html-num-fmt')) {
switch(
condition,
'!=' = x != value,
'!null' = !is.na(x),
'<' = x < value,
'<=' = x <= value,
'=' = x == value,
'>' = x > value,
'>=' = x >= value,
'between' = x >= value[1] & x <= value[2],
'!between' = x < value[1] | x > value[2],
'null' = is.na(x),
stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type))
)
} else if (type %in% c('date', 'moment', 'luxon')) {
switch(
condition,
'!=' = x != value,
'!null' = !is.na(x),
'<' = x < value,
'=' = x == value,
'>' = x > value,
'between' = x >= value[1] & x <= value[2],
'!between' = x < value[1] | x > value[2],
'null' = is.na(x),
stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type))
)
} else {
stop(sprintf('unsupported criteria type "%s"', type))
}
}
5 changes: 5 additions & 0 deletions R/shiny.R
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,11 @@ dataTablesFilter = function(data, params) {
# start searching with all rows
i = seq_len(n)

# apply SearchBuilder query if present
if (!is.null(s <- q$searchBuilder)) {
i = which(sbEvaluateSearch(s, data))
}

# search by columns
if (length(i)) for (j in names(q$columns)) {
col = q$columns[[j]]
Expand Down
43 changes: 43 additions & 0 deletions tests/testit/test-searchbuilder.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
library(testit)

assert('SearchBuilder condition evaluation works', {
(sbEvaluateCondition('>', 'num', 1:2, 1) == c(FALSE, TRUE))
(sbEvaluateCondition('between', 'num', 7, c(2, 4)) == FALSE)
(sbEvaluateCondition('starts', 'string', 'foo', 'f') == TRUE)
(sbEvaluateCondition('starts', 'string', factor('foo'), 'f') == TRUE)
(sbEvaluateCondition('null', 'string', c('', NA)) == c(TRUE, TRUE))
})

assert('SearchBuilder logic evaluation works', {
res = sbEvaluateSearch(
list(
logic = 'AND',
criteria = list(
list(condition = '<=', data = 'a', value = '4', type = 'num'),
list(condition = '>=', data = 'a', value = '2', type = 'num')
)
),
data.frame(a = 1:9)
)
(setequal(which(res), 2:4))
})

assert('SearchBuilder complex queries work', {
res = sbEvaluateSearch(
list(
logic = 'OR',
criteria = list(
list(condition = '=', data = 'a', value = '7', type = 'num'),
list(
logic = 'AND',
criteria = list(
list(condition = '<=', data = 'a', value = '4', type = 'num'),
list(condition = '>=', data = 'a', value = '2', type = 'num')
)
)
)
),
data.frame(a = 1:9)
)
(setequal(which(res), c(2:4, 7)))
})

0 comments on commit ce84fce

Please sign in to comment.