Skip to content

Commit 6cc0804

Browse files
authored
Merge pull request #34 from cynkra/19-inline-menu-editor
19 inline menu editor
2 parents 6191a3e + 3d24983 commit 6cc0804

File tree

11 files changed

+32326
-22
lines changed

11 files changed

+32326
-22
lines changed

R/cheetah.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ cheetah <- function(
4747
is_named_list(columns) & names(columns) %in% colnames(data)
4848
)
4949

50-
columns <- update_col_list_with_classes(data, columns) %>%
51-
add_field_to_list()
50+
columns <-
51+
update_col_list_with_classes(data, columns) %>%
52+
add_field_to_list()
5253

5354
data_json <- toJSON(data, dataframe = "rows")
5455
# forward options using x

R/cheetah_utils.R

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,32 @@
77
#' @param width Column width.
88
#' @param min_width Column minimal width.
99
#' @param max_width Column max width.
10-
#' @param column_type Column type. There are 6 possible options:
10+
#' @param column_type Column type. By default, the column type is inferred from the data type of the column.
11+
#' There are 7 possible options:
1112
#' \itemize{
1213
#' \item \code{"text"} for text columns.
1314
#' \item \code{"number"} for numeric columns.
1415
#' \item \code{"check"} for check columns.
1516
#' \item \code{"image"} for image columns.
1617
#' \item \code{"radio"} for radio columns.
17-
#' \item \code{"multilinetext"} for multiline text in columns.
18+
#' \item \code{"multilinetext"} for multiline text columns.
19+
#' \item \code{"menu"} for menu selection columns. If \code{column_type == "menu"},
20+
#' action parameter must be set to "inline_menu" and menu_options must be provided.
21+
#' Note: Works efficiently only in shiny.
1822
#' }
1923
#' @param action The action property defines column actions. Select
20-
#' the appropriate Action class for the column type. For instance,
21-
#' if the column type is \code{"text"}, the action can be \code{"input"}.
22-
#' There are 3 supported actions:
24+
#' the appropriate Action class for the column type.
2325
#' \itemize{
2426
#' \item \code{"input"} for input action columns.
2527
#' \item \code{"check"} for check action columns.
2628
#' \item \code{"radio"} for radio action columns.
29+
#' \item \code{"inline_menu"} for menu selection columns.
2730
#' }
31+
#' @param menu_options A list of menu options when using \code{column_type = "menu"}.
32+
#' Each option should be a list with \code{value} and \code{label} elements.
33+
#' The menu options must be a list of lists, each containing a \code{value}
34+
#' and \code{label} element.
35+
#' The \code{label} element is the label that will be displayed in the menu.
2836
#' @param style Column style.
2937
#' @param message Cell message. Expect a [htmlwidgets::JS()] function that
3038
#' takes `rec` as argument. It must return an object with two properties: `type` for the message
@@ -61,21 +69,46 @@ column_def <- function(
6169
max_width = NULL,
6270
column_type = NULL,
6371
action = NULL,
72+
menu_options = NULL,
6473
style = NULL,
6574
message = NULL,
6675
sort = FALSE
6776
) {
6877
check_column_type(column_type)
78+
check_action_type(action, column_type)
79+
in_shiny <- shiny::isRunning()
80+
81+
if (all(!is.null(column_type), column_type == "menu", !in_shiny)) {
82+
warning(
83+
"Dropdown menu action does not work properly outside a shiny environment"
84+
)
85+
}
86+
87+
if (
88+
all(!is.null(column_type), column_type == "menu", is.null(menu_options))
89+
) {
90+
stop("menu_options must be provided when column_type is 'menu'")
91+
}
92+
6993
if (!is.null(message) && !inherits(message, "JS_EVAL"))
7094
stop("message must be a JavaScript function wrapped by htmlwidgets::JS().")
95+
7196
list(
7297
caption = name,
7398
width = width,
7499
minWidth = min_width,
75100
maxWidth = max_width,
76101
columnType = column_type,
77-
action = action,
78-
style = style,
102+
action = if (!is.null(action)) {
103+
if (action == "inline_menu") {
104+
list(
105+
type = action,
106+
options = menu_options
107+
)
108+
} else {
109+
action
110+
}
111+
},
79112
message = message,
80113
sort = sort
81114
)

R/utils.R

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
is_testing <- function ()
2+
{
3+
identical(Sys.getenv("TESTTHAT"), "true")
4+
}
5+
16
is_named_list <- function(x) {
27
is.list(x) && !is.null(names(x)) && all(names(x) != "")
38
}
@@ -66,7 +71,7 @@ column_style_check <- function(columns) {
6671

6772
check_column_type <- function(x) {
6873
av_options <-
69-
c("text", "check", "number", "radio", "image", "multilinetext")
74+
c("text", "check", "number", "radio", "image", "multilinetext", "menu")
7075

7176
if (!is.null(x) && !(x %in% av_options)) {
7277
msg <- sprintf(
@@ -80,11 +85,24 @@ check_column_type <- function(x) {
8085

8186
update_col_list_with_classes <- function(data, col_list) {
8287
col_classes <- lapply(data, class)
88+
in_shiny <- shiny::isRunning()
89+
is_testing <- is_testing()
8390

8491
for (col_name in names(col_classes)) {
8592
if (is.null(col_list[[col_name]]$columnType)) {
8693
if (col_classes[[col_name]] == "numeric") {
8794
col_list[[col_name]]$columnType <- "number"
95+
} else if (col_classes[[col_name]] == "factor" && any(in_shiny, is_testing)) {
96+
# This is to recover the possible choices for a factor column.
97+
menu_opt <- lapply(
98+
unique(data[[col_name]]),
99+
\(val) {
100+
list(value = val, label = val)
101+
}
102+
)
103+
col_list[[col_name]]$columnType <- "menu"
104+
col_list[[col_name]]$action <-
105+
list(type = "inline_menu", options = menu_opt)
88106
} else {
89107
col_list[[col_name]]$columnType <- "text"
90108
}
@@ -93,3 +111,18 @@ update_col_list_with_classes <- function(data, col_list) {
93111

94112
col_list
95113
}
114+
115+
check_action_type <- function(action = NULL, column_type = NULL) {
116+
if (is.null(action)) return(invisible())
117+
118+
valid_actions <- c("input", "check", "radio", "inline_menu")
119+
if (!action %in% valid_actions) {
120+
stop("Invalid action type. Must be one of: ",
121+
paste(valid_actions, collapse = ", "))
122+
}
123+
124+
# Validate action-column type compatibility
125+
if (action == "inline_menu" && any(is.null(column_type), column_type != "menu")) {
126+
stop("'inline_menu' action can only be used with 'menu' column type")
127+
}
128+
}

inst/htmlwidgets/cheetah.js

Lines changed: 32144 additions & 2 deletions
Large diffs are not rendered by default.

man/column_def.Rd

Lines changed: 15 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

srcjs/widgets/cheetah.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,23 @@ HTMLWidgets.widget({
3131
...(userMap[item.field] || {})
3232
}));
3333

34+
// Iterate over the list and process the `action` property if it is not null or undefined
35+
columns.forEach((obj) => {
36+
if (obj.action != null) { // Checks for both null and undefined
37+
if (obj.action.type === "inline_menu") {
38+
obj.columnType = new cheetahGrid.columns.type.MenuColumn({
39+
options: obj.action.options,
40+
});
41+
42+
obj.action = new cheetahGrid.columns.action.InlineMenuEditor({
43+
options: obj.action.options,
44+
});
45+
}
46+
}
47+
});
48+
3449
} else {
35-
columns = defaultCol
50+
columns = defaultCol;
3651
}
3752

3853
const grid = new cheetahGrid.ListGrid({

tests/testthat/test-utils.R

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ test_that("test column style check", {
3939

4040
test_that("update_col_list_with_classes sets columnType correctly", {
4141
data <- data.frame(
42-
full_name = c("Alan Smith", "Mike John", "John Doe"),
43-
grade = c(78, 52, 3),
44-
passed = c(TRUE, FALSE, TRUE)
42+
full_name = c("Anne Smith", "Mike John", "John Doe", "Janet Jones"),
43+
grade = c(78, 52, 3, 27),
44+
passed = c(TRUE, TRUE, FALSE, FALSE),
45+
gender = c("female", "male", "male", "female")
4546
)
4647

48+
# Set 'gender' to a factor column
49+
data$gender <- as.factor(data$gender)
50+
4751
columns <- list(
4852
passed = list(columnType = "check"),
4953
full_name = list(name = "Names")
@@ -59,4 +63,8 @@ test_that("update_col_list_with_classes sets columnType correctly", {
5963

6064
# Column 'full_name' is (non-numeric), columnType becomes "text"
6165
expect_equal(updated_col_list$full_name$columnType, "text")
66+
67+
# Column 'gender' is a factor, columnType becomes "menu" and action to "inline_menu"
68+
expect_equal(updated_col_list$gender$columnType, "menu")
69+
expect_equal(updated_col_list$gender$action$type, "inline_menu")
6270
})

vignettes/cheetahR.qmd

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ The `column_type` parameter in `column_def()` allows you to specify different ty
6767
- `"image"`: For image columns
6868
- `"radio"`: For radio button columns
6969
- `"multilinetext"`: For multiline text columns
70+
- `"menu"`: For dropdown menu selection columns
7071

71-
The `column_type` parameter is optional. If it is not specified, the column type will be inferred from the data type.
72+
If `column_type` parameter is optional. If it is not specified, the column type will be inferred from the data type.
7273

7374
```{r}
7475
# Using checkbox column type to indicate NA values
@@ -165,3 +166,64 @@ cheetah(
165166
)
166167
)
167168
```
169+
170+
171+
## `cheetah()` usage in Shiny
172+
cheetahR works seamlessly in a Shiny app. You can use it in both the UI and server components. In the UI, simply call `cheetahR::cheetahOutput()` to create a placeholder for the grid. In the server, use `cheetahR::renderCheetah()` to render the grid with your data and options.
173+
174+
The grid will automatically update when the underlying data changes, making it perfect for reactive applications. All features like filtering, sorting, and custom column definitions work exactly the same way as in standalone R usage.
175+
176+
177+
One special feature that works particularly well in Shiny is the `menu` column type, which allows users to select from predefined options in a dropdown menu. This is ideal for interactive data editing workflows.
178+
179+
## Menu column in Shiny
180+
By default, `cheetah()` automatically detects any "factor" columns in your data and converts them into menu columns. A menu column displays a dropdown menu with predefined options that users can select from. This is particularly useful when you want to restrict input to a specific set of valid choices. For example, if you have a factor column with levels "Low", "Medium", and "High", it will be displayed as a dropdown menu with these three options.
181+
```{r, eval=FALSE}
182+
library(shiny)
183+
library(bslib)
184+
library(cheetahR)
185+
186+
187+
ui <- page_fluid(cheetahOutput("grid"))
188+
189+
server <- function(input, output) {
190+
output$grid <- renderCheetah({
191+
cheetah(data = iris)
192+
})
193+
}
194+
195+
shinyApp(ui = ui, server = server)
196+
```
197+
![Default menu column sample 1](figures/default_menucolumn_img_1.png)
198+
199+
![Default menu column sample 2](figures/default_menucolumn_img_2.png)
200+
201+
### Customizing the 'menu options'
202+
```{r, eval=FALSE}
203+
library(shiny)
204+
library(bslib)
205+
library(cheetahR)
206+
207+
ui <- page_fluid(cheetahOutput("grid"))
208+
209+
server <- function(input, output) {
210+
output$grid <- renderCheetah({
211+
cheetah(data = iris,
212+
columns = list(
213+
Species = column_def(
214+
column_type = "menu",
215+
action = "inline_menu",
216+
menu_options = list(
217+
setosa = "Option Setosa",
218+
versicolor = "Option Vericolor" ,
219+
virginica = "Option Virginica"
220+
)
221+
)
222+
)
223+
)
224+
})
225+
}
226+
227+
shinyApp(ui = ui, server = server)
228+
```
229+
![Customized menu column sample ](figures/customized_menucolumn_img.png)
193 KB
Loading
147 KB
Loading

0 commit comments

Comments
 (0)