Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions js/data-frame/cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,13 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
// style={{ width: "100%", height: "100%" }}
/>
);
} else if (isHtmlColumn) {
addToTableCellClass("cell-html");
} else {
// `isEditing` is `false`
// `isEditing` is `false` and not an HTML column, so we can allow for double clicks to go into edit mode

// Only allow transition to edit mode if the cell can be edited
if (editCellsIsAllowed && !isHtmlColumn) {
if (editCellsIsAllowed) {
addToTableCellClass("cell-editable");
onCellDoubleClick = (e: ReactMouseEvent<HTMLTableCellElement>) => {
// Do not prevent default or stop propagation here!
Expand All @@ -452,8 +454,6 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
obj_draft.editValue = getCellValueText(cellValue) as string;
});
};
} else {
addToTableCellClass("cell-html");
}
}
if (isShinyHtml(cellValue)) {
Expand Down
4 changes: 3 additions & 1 deletion js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
* Determines if the user is allowed to edit cells in the table.
*/
const editCellsIsAllowed = payloadOptions["editable"] === true;
("Barret");

/**
* Determines if any cell is currently being edited
Expand Down Expand Up @@ -711,6 +710,9 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
tabIndex={0}
onClick={header.column.getToggleSortingHandler()}
onKeyDown={makeHeaderKeyDown(header.column)}
className={
header.column.getCanSort() ? undefined : "header-html"
}
>
{headerContent}
</th>
Expand Down
12 changes: 9 additions & 3 deletions js/data-frame/sort-arrows.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SortDirection } from "@tanstack/react-table";
import React, { FC } from "react";

const sortClassName = "sort-arrow";
const sortCommonProps = {
className: "sort-arrow",
viewBox: [-1, -1, 2, 2].map((x) => x * 1.4).join(" "),
width: "100%",
height: "100%",
Expand All @@ -16,7 +16,10 @@ const sortPathCommonProps = {
};

const sortArrowUp = (
<svg xmlns="http://www.w3.org/2000/svg" {...sortCommonProps}>
<svg
xmlns="http://www.w3.org/2000/svg"
{...{ ...sortCommonProps, className: `${sortClassName} sort-arrow-up` }}
>
<path
d="M -1 0.5 L 0 -0.5 L 1 0.5"
{...sortPathCommonProps}
Expand All @@ -26,7 +29,10 @@ const sortArrowUp = (
);

const sortArrowDown = (
<svg xmlns="http://www.w3.org/2000/svg" {...sortCommonProps}>
<svg
xmlns="http://www.w3.org/2000/svg"
{...{ ...sortCommonProps, className: `${sortClassName} sort-arrow-down` }}
>
<path
d="M -1 -0.5 L 0 0.5 L 1 -0.5"
{...sortPathCommonProps}
Expand Down
210 changes: 151 additions & 59 deletions shiny/playwright/controller/_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from playwright.sync_api import FilePayload, FloatRect, Locator, Page, Position
from playwright.sync_api import expect as playwright_expect

from shiny.render._data_frame import ColumnFilter, ColumnSort

# Import `shiny`'s typing extentions.
# Since this is a private file, tell pyright to ignore the import
from ..._typing_extensions import TypeGuard, assert_type
Expand Down Expand Up @@ -4302,34 +4304,25 @@ class _CardValueBoxFullScreenM:
Represents a class for managing full screen functionality of a Card or Value Box.
"""

# TODO-karan-test: Convert `open_full_screen` and `close_full_screen` to `set_full_screen(open:bool)`
def open_full_screen(
self: _CardValueBoxFullScreenLayoutP, *, timeout: Timeout = None
) -> None:
"""
Opens the element in full screen mode.

Parameters
----------
timeout
The maximum time to wait for full screen mode to open. Defaults to `None`.
"""
self.loc_title.hover(timeout=timeout)
self._loc_fullscreen.wait_for(state="visible", timeout=timeout)
self._loc_fullscreen.click(timeout=timeout)

def close_full_screen(
self: _CardValueBoxFullScreenLayoutP, *, timeout: Timeout = None
def set_full_screen(
self: _CardValueBoxFullScreenLayoutP, open: bool, *, timeout: Timeout = None
) -> None:
"""
Exits full screen mode.
Sets the element to full screen mode or exits full screen mode.

Parameters
----------
open
`True` to open the element in full screen mode, `False` to exit full screen mode.
timeout
The maximum time to wait to wait for full screen mode to exit. Defaults to `None`.
The maximum time to wait for the operation to complete. Defaults to `None`.
"""
self._loc_close_button.click(timeout=timeout)
if open:
self.loc_title.hover(timeout=timeout)
self._loc_fullscreen.wait_for(state="visible", timeout=timeout)
self._loc_fullscreen.click(timeout=timeout)
else:
self._loc_close_button.click(timeout=timeout)

def expect_full_screen(
self: _CardValueBoxFullScreenLayoutP, value: bool, *, timeout: Timeout = None
Expand Down Expand Up @@ -6269,68 +6262,167 @@ def edit_cell(
cell.dblclick(timeout=timeout)
cell.locator("> textarea").fill(text)

# TODO-karan-test: Rename to `set_sort?`
# TODO-karan-test: Add support for a list of columns
# TODO-karan-test: Add support for direction
# TODO-karan-test: Add method for testing direction
def set_column_sort(
def set_sort(
self,
col: int,
sort: int | ColumnSort | list[int | ColumnSort] | None,
*,
timeout: Timeout = None,
) -> None:
):
"""
Sorts the column in the data frame.
Set or modify the sorting of columns in a table or grid component.
This method allows setting single or multiple column sorts, or resetting the sort order.

Parameters
----------
col
The column number to sort.
sort
The sorting configuration to apply. Can be one of the following:
int: Index of the column to sort by (ascending order by default).
ColumnSort: A dictionary specifying a single column sort with 'col' and 'desc' keys.
list[ColumnSort]: A list of dictionaries for multi-column sorting.
None: No sorting applied (not implemented in the current code).
timeout
The maximum time to wait for the action to complete. Defaults to `None`.
"""
self.loc_column_label.nth(col).click(timeout=timeout)

# TODO-karan-test: Rename to `set_filter?`
def click_loc(loc: Locator, *, shift: bool = False):
clickModifier: list[Literal["Shift"]] | None = (
["Shift"] if bool(shift) else None
)
loc.click(
timeout=timeout,
modifiers=clickModifier,
)
# Wait for arrows to react a little bit
# This could possible be changed to a `wait_for_change`, but 150ms should be fine
self.page.wait_for_timeout(150)

# Reset arrow sorting by clicking on the arrows until none are found
sortingArrows = self.loc_column_label.locator("svg.sort-arrow")
while sortingArrows.count() > 0:
click_loc(sortingArrows.first)

# Quit early if no sorting is needed
if sort is None:
return

if isinstance(sort, int) | isinstance(sort, dict) and not isinstance(
sort, list
):
sort = [sort]

if not isinstance(sort, list):
raise ValueError(
"Invalid sort value. Must be an int, ColumnSort, list[ColumnSort], or None."
)

# For every sorting info...
for sort_info, i in zip(sort, range(len(sort))):
# TODO-barret-future; assert column does not have `cell-html` class
shift = i > 0

if isinstance(sort_info, int):
sort_info = {"col": sort_info}

# Verify ColumnSortInfo
assert isinstance(
sort_info, dict
), f"Invalid sort value at position {i}. Must be an int, ColumnSort, list[ColumnSort], or None."
assert (
"col" in sort_info
), f"Column index (`col`) at position {i} is required for sorting."

sort_col = self.loc_column_label.nth(sort_info["col"])
expect_not_to_have_class(sort_col, "header-html")

# If no `desc` key is found, click the column to sort and move on
if "desc" not in sort_info:
click_loc(sort_col, shift=shift)
continue

# "desc" in sort_info
desc_val = bool(sort_info["desc"])
sort_col.scroll_into_view_if_needed()
for _ in range(2):
if desc_val:
# If a descending is found, stop clicking
if sort_col.locator("svg.sort-arrow-down").count() > 0:
break
else:
# If a ascending is found, stop clicking
if sort_col.locator("svg.sort-arrow-up").count() > 0:
break
click_loc(sort_col, shift=shift)

# TODO-karan-test: Add support for a list of columns ? If so, all other columns should be reset
# TODO-karan-test: Add support for a None value reset all filters
# TODO-karan-test: Add method for testing direction
def set_column_filter(
def set_filter(
self,
col: int,
# TODO-barret support array of filters
filter: ColumnFilter | list[ColumnFilter] | None,
*,
text: str | list[str] | tuple[str, str],
timeout: Timeout = None,
) -> None:
):
"""
Filters the column in the data frame.
Set or reset filters for columns in a table or grid component.
This method allows setting string filters, numeric range filters, or clearing all filters.

Parameters
----------
col
The column number to filter.
text
The text to filter the column.
filter
The filter to apply. Can be one of the following:
None: Resets all filters.
str: A string filter (deprecated, use ColumnFilterStr instead).
ColumnFilterStr: A dictionary specifying a string filter with 'col' and 'value' keys.
ColumnFilterNumber: A dictionary specifying a numeric range filter with 'col' and 'value' keys.
timeout
The maximum time to wait for the action to complete. Defaults to `None`.
"""
if isinstance(text, str):
self.loc_column_filter.nth(col).locator("> input").fill(
text,
timeout=timeout,
)
else:
assert len(text) == 2
header_inputs = self.loc_column_filter.nth(col).locator("> div > input")
header_inputs.nth(0).fill(
text[0],
timeout=timeout,
)
header_inputs.nth(1).fill(
text[1],
timeout=timeout,
# reset all filters
all_input_handles = self.loc_column_filter.locator(
"> input, > div > input"
).element_handles()
for input_handle in all_input_handles:
input_handle.scroll_into_view_if_needed()
input_handle.fill("", timeout=timeout)

if filter is None:
return

if isinstance(filter, dict):
filter = [filter]

if not isinstance(filter, list):
raise ValueError(
"Invalid filter value. Must be a ColumnFilter, list[ColumnFilter], or None."
)

for filterInfo in filter:
if "col" not in filterInfo:
raise ValueError("Column index (`col`) is required for filtering.")

if "value" not in filterInfo:
raise ValueError("Filter value (`value`) is required for filtering.")

filterColumn = self.loc_column_filter.nth(filterInfo["col"])

if isinstance(filterInfo["value"], str):
filterColumn.locator("> input").fill(filterInfo["value"])
elif isinstance(filterInfo["value"], (tuple, list)):
header_inputs = filterColumn.locator("> div > input")
if filterInfo["value"][0] is not None:
header_inputs.nth(0).fill(
str(filterInfo["value"][0]),
timeout=timeout,
)
if filterInfo["value"][1] is not None:
header_inputs.nth(1).fill(
str(filterInfo["value"][1]),
timeout=timeout,
)
else:
raise ValueError(
"Invalid filter value. Must be a string or a tuple/list of two numbers."
)

def save_cell(
self,
text: str,
Expand Down
8 changes: 4 additions & 4 deletions shiny/www/shared/py-shiny/data-frame/data-frame.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions shiny/www/shared/py-shiny/data-frame/data-frame.js.map

Large diffs are not rendered by default.

Loading