Skip to content

Commit

Permalink
Merge branch 'main' into split_df_mode
Browse files Browse the repository at this point in the history
* main:
  feat: Create `Session.set_message_handler(name, handler)` (#1253)
  Fix input name
  Enable error console when running locally. (#1060)
  test: Update test to use variable, not capture stdout (#1236)
  • Loading branch information
schloerke committed Mar 27, 2024
2 parents 6b3a6af + 10b8e68 commit 702f1e9
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 340 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New features

* `Session` objects now have a `set_message_handler(name, fn)` method that allows you to register a message handler function that will be called when a request message with the given name is received from the client (via `Shiny.shinyapp.makeRequest()` (JS)). (#1253)

* Experimental: `@render.data_frame` return values of `DataTable` and `DataGrid` support `mode="edit"` to enable editing of the data table cells. (#1198)

* `ui.card()` and `ui.value_box()` now take an `id` argument that, when provided, is used to report the full screen state of the card or value box to the server. For example, when using `ui.card(id = "my_card", full_screen = TRUE)` you can determine if the card is currently in full screen mode by reading the boolean value of `input.my_card()["full_screen"]`. (#1215)
Expand All @@ -23,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `ui.value_box()`, `ui.layout_columns()` and `ui.layout_column_wrap()` now all have `min_height` and `max_height` arguments. These are useful in filling layouts, like `ui.page_fillable()`, `ui.page_sidebar(fillable=True)` or `ui.page_navbar(fillable=True)`. For example, you can use `ui.layout_columns(min_height=300, max_height=500)` to ensure that a set of items (likely arranged in a row of columns) are always between 300 and 500 pixels tall. (#1223)

* Added an error console which displays errors in the browser's UI. This is enabled by default when running applications locally, and can be disabled with `shiny run --no-dev-mode`. It is not enabled for applications that are deployed to a server. (#1060)

### Bug fixes

* On Windows, Shiny Express app files are now read in as UTF-8. (#1203)
Expand Down
7 changes: 6 additions & 1 deletion js/dataframe/cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getCellEditMapValue,
} from "./cell-edit-map";
import { updateCellsData } from "./data-update";
import type { PatchInfo } from "./types";

// States
// # √ Ready
Expand Down Expand Up @@ -50,6 +51,7 @@ export type CellState = keyof typeof CellStateEnum;
interface TableBodyCellProps {
id: string | null;
cell: Cell<unknown[], unknown>;
patchInfo: PatchInfo;
columns: readonly string[];
editCellsIsAllowed: boolean;
editRowIndex: number | null;
Expand All @@ -66,6 +68,7 @@ interface TableBodyCellProps {
export const TableBodyCell: FC<TableBodyCellProps> = ({
id,
cell,
patchInfo,
columns,
editCellsIsAllowed,
editRowIndex,
Expand Down Expand Up @@ -137,7 +140,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
// * When a td is focused, Have esc key move focus to the table
// * When table is focused, Have esc key blur the focus
// TODO-barret-future; Combat edit mode being independent of selection mode
// * In row / column selection mode, allow for arrowoutput_binding_request_handler key navigation by focusing on a single cell, not a TR
// * In row / column selection mode, allow for arrow key navigation by focusing on a single cell, not a TR
// * If a cell is focused,
// * `enter key` allows you to go into edit mode; If editing is turned off, the selection is toggled
// * `space key` allows you toggle the selection of the cell
Expand Down Expand Up @@ -253,6 +256,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
// updateCellsData updates the underlying data via `setData` and `setCellEditMap`
updateCellsData({
id,
patchInfo: patchInfo,
patches: [
{
rowIndex,
Expand All @@ -273,6 +277,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
});
}, [
id,
patchInfo,
rowIndex,
columnIndex,
value,
Expand Down
9 changes: 4 additions & 5 deletions js/dataframe/data-update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ResponseValue, makeRequestPromise } from "./request";
import type { CellState } from "./cell";
import { CellStateEnum } from "./cell";
import { CellEdit, SetCellEditMap, makeCellEditMapKey } from "./cell-edit-map";
import type { PatchInfo } from "./types";

export type CellPatch = {
rowIndex: number;
Expand All @@ -19,6 +20,7 @@ export type CellPatchPy = {

export function updateCellsData({
id,
patchInfo,
patches,
onSuccess,
onError,
Expand All @@ -27,6 +29,7 @@ export function updateCellsData({
setCellEditMap,
}: {
id: string | null;
patchInfo: PatchInfo;
patches: CellPatch[];
onSuccess: (values: CellPatch[]) => void;
onError: (err: string) => void;
Expand All @@ -47,12 +50,8 @@ export function updateCellsData({
});

makeRequestPromise({
method: "output_binding_request_handler",
method: patchInfo.key,
args: [
// id: string
id,
// handler: string
"patches",
// list[CellPatch]
patchesPy,
],
Expand Down
21 changes: 15 additions & 6 deletions js/dataframe/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { SortArrow } from "./sort-arrows";
import css from "./styles.scss";
import { useTabindexGroup } from "./tabindex-group";
import { useSummary } from "./table-summary";
import { EditModeEnum, PandasData, TypeHint } from "./types";
import { EditModeEnum, PandasData, PatchInfo, TypeHint } from "./types";

// TODO-barret set selected cell as input! (Might be a followup?)

Expand Down Expand Up @@ -90,11 +90,16 @@ declare module "@tanstack/table-core" {
interface ShinyDataGridProps<TIndex> {
id: string | null;
data: PandasData<TIndex>;
patchInfo: PatchInfo;
bgcolor?: string;
}

const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = (props) => {
const { id, data, bgcolor } = props;
const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
id,
data,
patchInfo,
bgcolor,
}) => {
const { columns, type_hints: typeHints, data: rowData } = data;
const { width, height, fill, filters: withFilters } = data.options;

Expand Down Expand Up @@ -486,6 +491,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = (props) => {
id={id}
key={cell.id}
cell={cell}
patchInfo={patchInfo}
editCellsIsAllowed={editCellsIsAllowed}
columns={columns}
editRowIndex={editRowIndex}
Expand Down Expand Up @@ -673,10 +679,12 @@ export class ShinyDataFrameOutput extends HTMLElement {
}
}

renderValue(data: unknown) {
renderValue(
value: null | { patchInfo: PatchInfo; data: PandasData<unknown> }
) {
this.clearError();

if (!data) {
if (!value) {
this.reactRoot!.render(null);
return;
}
Expand All @@ -685,7 +693,8 @@ export class ShinyDataFrameOutput extends HTMLElement {
<StrictMode>
<ShinyDataGrid
id={this.id}
data={data as PandasData<unknown>}
data={value.data}
patchInfo={value.patchInfo}
bgcolor={getComputedBgColor(this)}
></ShinyDataGrid>
</StrictMode>
Expand Down
4 changes: 4 additions & 0 deletions js/dataframe/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ export interface PandasData<TIndex> {
type_hints?: ReadonlyArray<TypeHint>;
options: DataGridOptions;
}

export interface PatchInfo {
key: string;
}
4 changes: 2 additions & 2 deletions shiny/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def _render_page(self, ui: Tag | TagList, lib_prefix: str) -> RenderedHTML:
ui_res = copy.copy(ui)
# Make sure requirejs, jQuery, and Shiny come before any other dependencies.
# (see require_deps() for a comment about why we even include it)
ui_res.insert(0, [require_deps(), jquery_deps(), shiny_deps()])
ui_res.insert(0, [require_deps(), jquery_deps(), *shiny_deps()])
rendered = HTMLDocument(ui_res).render(lib_prefix=lib_prefix)
self._ensure_web_dependencies(rendered["dependencies"])
return rendered
Expand All @@ -449,7 +449,7 @@ def _render_page_from_file(self, file: Path, lib_prefix: str) -> RenderedHTML:

doc = HTMLTextDocument(
page_html,
deps=[require_deps(), jquery_deps(), shiny_deps()],
deps=[require_deps(), jquery_deps(), *shiny_deps()],
deps_replace_pattern='<meta name="shiny-dependency-placeholder" content="">',
)

Expand Down
12 changes: 12 additions & 0 deletions shiny/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ def main() -> None:
help="Launch app browser after app starts, using the Python webbrowser module.",
show_default=True,
)
@click.option(
"--dev-mode/--no-dev-mode",
is_flag=True,
default=True,
help="Dev mode",
show_default=True,
)
@no_example()
def run(
app: str | shiny.App,
Expand All @@ -159,6 +166,7 @@ def run(
app_dir: str,
factory: bool,
launch_browser: bool,
dev_mode: bool,
**kwargs: object,
) -> None:
reload_includes_list = reload_includes.split(",")
Expand All @@ -177,6 +185,7 @@ def run(
app_dir=app_dir,
factory=factory,
launch_browser=launch_browser,
dev_mode=dev_mode,
**kwargs,
)

Expand All @@ -196,6 +205,7 @@ def run_app(
app_dir: Optional[str] = ".",
factory: bool = False,
launch_browser: bool = False,
dev_mode: bool = True,
**kwargs: object,
) -> None:
"""
Expand Down Expand Up @@ -276,6 +286,8 @@ def run_app(

os.environ["SHINY_HOST"] = host
os.environ["SHINY_PORT"] = str(port)
if dev_mode:
os.environ["SHINY_DEV_MODE"] = "1"

if isinstance(app, str):
# Remove ":app" suffix if present. Normally users would just pass in the
Expand Down
5 changes: 3 additions & 2 deletions shiny/api-examples/sidebar/app-core-dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
ui.layout_column_wrap(
ui.card(
ui._card.card_body(
ui.output_ui("sidebar_dynamic", fill=True, fillable=True), class_="p-0"
ui.output_ui("sidebar_dynamic_container", fill=True, fillable=True),
class_="p-0",
),
),
ui.card(
Expand All @@ -39,7 +40,7 @@

def server(input: Inputs, output: Outputs, session: Session):
@render.ui
def sidebar_dynamic():
def sidebar_dynamic_container():
return ui.layout_sidebar(
ui.sidebar(
ui.markdown(
Expand Down
32 changes: 24 additions & 8 deletions shiny/html_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
from __future__ import annotations

import os

from htmltools import HTMLDependency


def shiny_deps() -> HTMLDependency:
return HTMLDependency(
name="shiny",
version="0.0.1",
source={"package": "shiny", "subdir": "www/shared/"},
script={"src": "shiny.js"},
stylesheet={"href": "shiny.min.css"},
)
def shiny_deps() -> list[HTMLDependency]:
deps = [
HTMLDependency(
name="shiny",
version="0.0.1",
source={"package": "shiny", "subdir": "www/shared/"},
script={"src": "shiny.js"},
stylesheet={"href": "shiny.min.css"},
)
]
if os.getenv("SHINY_DEV_MODE") == "1":
deps.append(
HTMLDependency(
"shiny-devmode",
version="0.0.1",
head="<script>window.__SHINY_DEV_MODE__ = true;</script>",
)
)

return deps


def jquery_deps() -> HTMLDependency:
Expand Down
44 changes: 32 additions & 12 deletions shiny/render/_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# TODO-barret; Docs
# TODO-barret; Add examples!
from typing import TYPE_CHECKING, Any, TypeVar
from typing import TYPE_CHECKING, Any, Awaitable, Callable, TypeVar

from htmltools import Tag

Expand All @@ -30,7 +30,7 @@
)

# as_selection_location_js,
from .renderer import Jsonifiable, Renderer, ValueFn, output_binding_request_handler
from .renderer import Jsonifiable, Renderer, ValueFn

if TYPE_CHECKING:
import pandas as pd
Expand Down Expand Up @@ -351,19 +351,31 @@ async def patches_fn(

self.set_patch_fn(patch_fn)
self.set_patches_fn(patches_fn)
# self._add_message_handlers()

# # TODO-barret; Use dynamic route instead of message handlers? Gut all of `output_binding_request_handler?` TODO: Talk with Joe
# session = get_current_session()
# if session is not None:
# session.dynamic_route("data-frame-patches-{id}", self._handle_patches)
def _set_patches_handler_impl(
self,
handler: Callable[..., Awaitable[Jsonifiable]] | None,
) -> str:
session = self._get_session()
key = session.set_message_handler(
f"data_frame_patches_{self.output_id}",
handler,
)
return key

# output_binding_request_handler(session, "name", self._handle_patches_1)
def _reset_patches_handler(self) -> str:
return self._set_patches_handler_impl(None)

def _set_patches_handler(self) -> str:
"""
Set the client patches handler for the data frame.
This method **must be** called as late as possible as it depends on the ID of the output.
"""
return self._set_patches_handler_impl(self._patches_handler)

# To be called by session's output_binding_request_handler message handler on this data_frame instance
@output_binding_request_handler
# Do not change this method name unless you update corresponding code in `/js/dataframe/`!!
async def _handle_patches(self, patches: list[CellPatch]) -> Jsonifiable:
async def _patches_handler(self, patches: list[CellPatch]) -> Jsonifiable:
assert_patches_shape(patches)

# Call user's cell update method to retrieve formatted values
Expand Down Expand Up @@ -446,6 +458,7 @@ def _set_output_metadata(self, *, output_id: str) -> None:
async def render(self) -> Jsonifiable:
# Reset value
self._reset_reactives()
self._reset_patches_handler()

value = await self.fn()
if value is None:
Expand All @@ -459,8 +472,15 @@ async def render(self) -> Jsonifiable:
)
)

# Send info to client
patch_key = self._set_patches_handler()
self._value.set(value)
return value.to_payload()
return {
"data": value.to_payload(),
"patchInfo": {
"key": patch_key,
},
}

async def _send_message_to_browser(self, handler: str, obj: dict[str, Any]):

Expand Down
4 changes: 1 addition & 3 deletions shiny/render/renderer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from ._dispatch import output_binding_request_handler
from ._renderer import ( # noqa: F401
from ._renderer import (
AsyncValueFn,
Jsonifiable,
Renderer,
Expand All @@ -13,5 +12,4 @@
"Jsonifiable",
"AsyncValueFn",
"RendererT",
"output_binding_request_handler",
)
Loading

0 comments on commit 702f1e9

Please sign in to comment.