-
Notifications
You must be signed in to change notification settings - Fork 1
F improve cell message #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
1f92740
Add js_ifelse()
olajoke 2840ab6
Add test for js_ifelse()
olajoke 6938cea
Update js_ifelse() and documentation
olajoke e120df9
Add add_cell_message()
olajoke d746afd
Export and document
olajoke 4994235
Update column_def()
olajoke 42213c7
Add test for add_cell_message()
olajoke 46e75af
Update vignetttes with add_cell_message() samples
olajoke e1ca7a7
Merge branch 'development' into f-improve-cell-message
olajoke d68d860
Set to the center instead
olajoke 57afde1
Update param options for js_false()
olajoke 08464b0
Use 'match.arg' instead
olajoke ec25971
- Add R helper functions to provide a simpler interface to create cel…
olajoke 75729c8
Merge branch 'development' into f-improve-cell-message
olajoke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
#' Create a JavaScript cell message function for cheetahR widgets | ||
#' | ||
#' Generates a JS function (wrapped with `htmlwidgets::JS`) that, | ||
#' given a record (`rec`), returns an object with `type` and `message`. | ||
#' | ||
#' @param type A string that specifies the type of message. | ||
#' One of `"error"`, `"warning"`, or `"info"`. Default is `"error"`. | ||
#' @param message A string or JS expression. If it contains `rec.`, `?`, `:`, | ||
#' or a trailing `;`, it is treated as raw JS (no additional quoting). | ||
#' Otherwise, it is escaped and wrapped in single quotes. | ||
#' | ||
#' @return A `htmlwidgets::JS` object containing a JavaScript function definition: | ||
#'```js | ||
#' function(rec) { | ||
#' return { | ||
#' type: "<type>", | ||
#' message: <message> | ||
#' }; | ||
#' } | ||
#'``` | ||
#' Use this within `column_def()` for cell validation | ||
#' | ||
#' @examples | ||
#' set.seed(123) | ||
#' iris_rows <- sample(nrow(iris), 10) | ||
#' data <- iris[iris_rows, ] | ||
#' | ||
#' # Simple warning | ||
#' cheetah( | ||
#' data, | ||
#' columns = list( | ||
#' Species = column_def( | ||
#' message = add_cell_message(type = "info", message = "Ok") | ||
#' ) | ||
#' ) | ||
#' ) | ||
#' | ||
#' # Conditional error using `js_ifelse()` | ||
#' cheetah( | ||
#' data, | ||
#' columns = list( | ||
#' Species = column_def( | ||
#' message = add_cell_message( | ||
#' message = js_ifelse(Species == "setosa", "", "Invalid") | ||
#' ) | ||
#' ) | ||
#' ) | ||
#' ) | ||
#' | ||
#' # Directly using a JS expression as string | ||
#' cheetah( | ||
#' data, | ||
#' columns = list( | ||
#' Sepal.Width = column_def( | ||
#' style = list(textAlign = "left"), | ||
#' message = add_cell_message( | ||
#' type = "warning", | ||
#' message = "rec['Sepal.Width'] <= 3 ? 'NarrowSepal' : 'WideSepal';" | ||
#' ) | ||
#' ) | ||
#' ) | ||
#' ) | ||
#' | ||
#' @export | ||
add_cell_message <- function( | ||
type = c("error", "warning", "info"), | ||
message = "message" | ||
) { | ||
type <- rlang::arg_match(type) | ||
|
||
is_js_expr <- grepl("rec\\.|\\?|\\:|;$", message) | ||
js_msg <- if (is_js_expr) { | ||
sub(";$", "", message) | ||
} else { | ||
msg_esc <- gsub("'", "\\'", message) | ||
paste0("'", msg_esc, "'") | ||
} | ||
|
||
js_fn <- paste0( | ||
"function(rec) {\n", | ||
" return {\n", | ||
" type: '", | ||
type, | ||
"',\n", | ||
" message: ", | ||
js_msg, | ||
"\n", | ||
" };\n", | ||
"}" | ||
) | ||
|
||
htmlwidgets::JS(js_fn) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
#' Convert an R logical expression into a JS ternary expression | ||
#' | ||
#' @param condition An R logical expression (supports %in% / %notin% / grepl() / comparisons / & |) | ||
#' @param true_result String to return when the condition is TRUE. Default is an empty string, which interprets as `null` in JS. | ||
#' @param false_result String to return when the condition is FALSE. Default is an empty string, which interprets as `null` in JS. | ||
#' @return A single character string containing a JavaScript ternary expression. | ||
#' @export | ||
js_ifelse <- function(condition, true_result = "", false_result = "") { | ||
olajoke marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
# 1) Capture the unevaluated condition as a single string | ||
txt <- paste(deparse(substitute(condition)), collapse = " ") | ||
|
||
# 2) Force all " to ' in the condition itself | ||
txt <- gsub("\"", "'", txt, fixed = TRUE) | ||
|
||
# 3) Handle %notin% and %in% | ||
handle_in <- function(text) { | ||
pat <- "(\\b[[:alnum:]_.]+)\\s*%(not)?in%\\s*c\\(([^)]+)\\)" | ||
matches <- gregexpr(pat, text, perl = TRUE)[[1]] | ||
if (matches[1] == -1) return(text) | ||
|
||
for (raw in regmatches(text, gregexpr(pat, text, perl = TRUE))[[1]]) { | ||
parts <- regmatches(raw, regexec(pat, raw, perl = TRUE))[[1]] | ||
var <- parts[2] | ||
neg <- !is.na(parts[3]) && nzchar(parts[3]) | ||
vals <- strsplit(parts[4], ",")[[1]] | ||
vals <- trimws(gsub("^['\"]|['\"]$", "", vals)) | ||
|
||
# build a single-quoted JS array literal | ||
arr <- sprintf("['%s']", paste(vals, collapse = "','")) | ||
expr <- sprintf("%s.includes(rec['%s'])", arr, var) | ||
if (neg) expr <- paste0("!", expr) | ||
|
||
text <- sub(pat, expr, text, perl = TRUE) | ||
} | ||
text | ||
} | ||
txt <- handle_in(txt) | ||
|
||
# 4) Handle grepl() and !grepl(), capturing the inner regex | ||
handle_grep <- function(text) { | ||
pat <- "(!?)grepl\\((['\"])(.*?)\\2,\\s*([[:alnum:]_.]+)\\)" | ||
matches <- gregexpr(pat, text, perl = TRUE)[[1]] | ||
if (matches[1] == -1) return(text) | ||
|
||
for (raw in regmatches(text, gregexpr(pat, text, perl = TRUE))[[1]]) { | ||
parts <- regmatches(raw, regexec(pat, raw, perl = TRUE))[[1]] | ||
notp <- nzchar(parts[2]) | ||
pattern <- parts[4] | ||
var <- parts[5] | ||
|
||
expr <- sprintf("%s/%s/.test(rec['%s'])", | ||
if (notp) "!" else "", pattern, var) | ||
text <- sub(pat, expr, text, perl = TRUE) | ||
} | ||
text | ||
} | ||
txt <- handle_grep(txt) | ||
|
||
# 5) Replace R logical operators with JS equivalents | ||
txt <- gsub("!=", "!==", txt, fixed = TRUE) | ||
txt <- gsub("==", "===", txt, fixed = TRUE) | ||
txt <- gsub(" & ", " && ", txt, fixed = TRUE) | ||
txt <- gsub(" | ", " || ", txt, fixed = TRUE) | ||
|
||
# 6) Prefix all remaining bare names with rec['...'], skipping 'rec' itself | ||
prefix_pat <- "(?<![\\w'\"/\\[\\]])\\b(?!rec\\b)([A-Za-z_][A-Za-z0-9_.]*)\\b(?![\\]\\('\"/])" | ||
txt <- gsub(prefix_pat, "rec['\\1']", txt, perl = TRUE) | ||
|
||
# 7) Wrap the true/false results in single quotes (or null) | ||
wrap <- function(x) { | ||
x2 <- gsub("\"", "'", x, fixed = TRUE) | ||
if (x2 == "") "null" else sprintf("'%s'", x2) | ||
} | ||
tval <- wrap(true_result) | ||
fval <- wrap(false_result) | ||
|
||
# 8) Assemble and return the JS ternary | ||
sprintf("%s ? %s : %s;", txt, tval, fval) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
test_that("js_ifelse conversion", { | ||
# Simple logic: numeric comparisons and equality | ||
expect_equal( | ||
js_ifelse(Sepal.Length > 5, "BigSepal", ""), | ||
"rec['Sepal.Length'] > 5 ? 'BigSepal' : null;" | ||
) | ||
expect_equal( | ||
js_ifelse(Species == "setosa", "IsSetosa", ""), | ||
"rec['Species'] === 'setosa' ? 'IsSetosa' : null;" | ||
) | ||
expect_equal( | ||
js_ifelse(Sepal.Width <= 3, "NarrowSepal", "WideSepal"), | ||
"rec['Sepal.Width'] <= 3 ? 'NarrowSepal' : 'WideSepal';" | ||
) | ||
|
||
# Combined logic | ||
expr <- js_ifelse(Sepal.Length > 5 & Species %notin% c("setosa"), "E", "X") | ||
expect_true(grepl("rec['Sepal.Length'] > 5 && !['setosa'].includes", expr, fixed = TRUE)) | ||
|
||
# Truthiness of bare variable | ||
expect_equal( | ||
js_ifelse(Species, "", "Please check."), | ||
"rec['Species'] ? null : 'Please check.';" | ||
) | ||
|
||
# Basic %in% and %notin% | ||
expect_equal( | ||
js_ifelse(Species %in% c("setosa", "virginica"), "Bad", ""), | ||
"['setosa',''virginica'].includes(rec['Species']) ? 'Bad' : null;" | ||
) | ||
expect_equal( | ||
js_ifelse(Species %notin% c("setosa"), "OK", ""), | ||
"!['setosa'].includes(rec['Species']) ? 'OK' : null;" | ||
) | ||
|
||
# grepl() and !grepl() | ||
expect_equal( | ||
js_ifelse(grepl("^vir", Species), "Yes", ""), | ||
"/^vir/.test(rec['Species']) ? 'Yes' : null;" | ||
) | ||
expect_equal( | ||
js_ifelse(!grepl("set", Species), "NoSet", ""), | ||
"!/set/.test(rec['Species']) ? 'NoSet' : null;" | ||
) | ||
}) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.