Skip to content

Commit

Permalink
first pass at update_cell
Browse files Browse the repository at this point in the history
  • Loading branch information
schloerke committed Oct 4, 2024
1 parent fa9f8d4 commit 8eec04d
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 107 deletions.
96 changes: 63 additions & 33 deletions js/data-frame/data-update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,62 @@ export type CellPatchPy = {
// prev: unknown;
};

type SetDataFn = (fn: (draft: unknown[][]) => void) => void;

export function addPatchToData({
setData,
newPatches,
setCellEditMapAtLoc,
}: {
setData: SetDataFn;
newPatches: CellPatch[];
setCellEditMapAtLoc: SetCellEditMapAtLoc;
}): void {
// Update data
setData((draft) => {
newPatches.forEach(({ rowIndex, columnIndex, value }) => {
draft[rowIndex]![columnIndex] = value;
});
});
// Set the new patches in cell edit map info
newPatches.forEach(({ rowIndex, columnIndex, value }) => {
setCellEditMapAtLoc(rowIndex, columnIndex, (obj_draft) => {
obj_draft.value = value;
obj_draft.state = CellStateEnum.EditSuccess;
// Remove save_error if it exists
obj_draft.errorTitle = undefined;
});
});
}

export function cellPatchPyArrToCellPatchArr(
patchesPy: CellPatchPy[]
): CellPatch[] {
const patches: CellPatch[] = patchesPy.map(
(patch: CellPatchPy): CellPatch => {
return {
rowIndex: patch.row_index,
columnIndex: patch.column_index,
value: patch.value,
};
}
);
return patches;
}

export function cellPatchArrToCellPatchPyArr(
patches: CellPatch[]
): CellPatchPy[] {
const patchesPy: CellPatchPy[] = patches.map((patch) => {
return {
row_index: patch.rowIndex,
column_index: patch.columnIndex,
value: patch.value,
};
});
return patchesPy;
}

export function updateCellsData({
patchInfo,
patches,
Expand All @@ -31,20 +87,13 @@ export function updateCellsData({
onSuccess: (values: CellPatch[]) => void;
onError: (err: string) => void;
columns: readonly string[];
setData: (fn: (draft: unknown[][]) => void) => void;
setData: SetDataFn;
setCellEditMapAtLoc: SetCellEditMapAtLoc;
}) {
// // Skip page index reset until after next rerender
// skipAutoResetPageIndex();

const patchesPy: CellPatchPy[] = patches.map((patch) => {
return {
row_index: patch.rowIndex,
column_index: patch.columnIndex,
value: patch.value,
// prev: patch.prev,
};
});
const patchesPy = cellPatchArrToCellPatchPyArr(patches);

makeRequestPromise({
method: patchInfo.key,
Expand All @@ -70,21 +119,7 @@ export function updateCellsData({
}
newPatchesPy = newPatchesPy as CellPatchPy[];

const newPatches: CellPatch[] = newPatchesPy.map(
(patch: CellPatchPy): CellPatch => {
return {
rowIndex: patch.row_index,
columnIndex: patch.column_index,
value: patch.value,
};
}
);

setData((draft) => {
newPatches.forEach(({ rowIndex, columnIndex, value }) => {
draft[rowIndex]![columnIndex] = value;
});
});
const newPatches = cellPatchPyArrToCellPatchArr(newPatchesPy);

// Set the old patches locations back to success state
// This may be overkill, but it guarantees that the incoming patches exit the saving state
Expand All @@ -99,15 +134,10 @@ export function updateCellsData({
obj_draft.errorTitle = undefined;
});
});
// Set the new patches
newPatches.forEach(({ rowIndex, columnIndex, value }) => {
setCellEditMapAtLoc(rowIndex, columnIndex, (obj_draft) => {
obj_draft.value = value;
obj_draft.state = CellStateEnum.EditSuccess;
// Remove save_error if it exists
obj_draft.errorTitle = undefined;
});
});

// Update data and cell edit map with new patches
addPatchToData({ setData, newPatches, setCellEditMapAtLoc });

onSuccess(newPatches);
})
.catch((err: string) => {
Expand Down
37 changes: 37 additions & 0 deletions js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import { ErrorsMessageValue } from "rstudio-shiny/srcts/types/src/shiny/shinyapp
import { useImmer } from "use-immer";
import { TableBodyCell } from "./cell";
import { getCellEditMapObj, useCellEditMap } from "./cell-edit-map";
import {
addPatchToData,
cellPatchPyArrToCellPatchArr,
type CellPatchPy,
} from "./data-update";
import { findFirstItemInView, getStyle } from "./dom-utils";
import { ColumnFiltersState, Filter, FilterValue, useFilters } from "./filter";
import type { CellSelection, SelectionModesProp } from "./selection";
Expand Down Expand Up @@ -463,6 +468,38 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
Shiny.renderDependenciesAsync([...htmlDeps]);
}, [htmlDeps]);

useEffect(() => {
const handleAddPatches = (
event: CustomEvent<{
patches: CellPatchPy[];
}>
) => {
const evtPatches = event.detail.patches;
const newPatches = cellPatchPyArrToCellPatchArr(evtPatches);

// Update data with extra patches
addPatchToData({
setData: setTableData,
newPatches,
setCellEditMapAtLoc,
});
};

if (!id) return;

const element = document.getElementById(id);
if (!element) return;

element.addEventListener("addPatches", handleAddPatches as EventListener);

return () => {
element.removeEventListener(
"addPatches",
handleAddPatches as EventListener
);
};
}, [columns, id, setCellEditMapAtLoc, setSorting, setTableData]);

useEffect(() => {
const handleColumnSort = (
event: CustomEvent<{ sort: { col: number; desc: boolean }[] }>
Expand Down
157 changes: 90 additions & 67 deletions shiny/render/_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,12 +495,20 @@ def _reset_reactives(self) -> None:
self._cell_patch_map.set({})

def _init_reactives(self) -> None:
with session_context(self._get_session()):

# Init
self._value: reactive.Value[
None | DataGrid[IntoDataFrameT] | DataTable[IntoDataFrameT]
] = reactive.Value(None)
self._cell_patch_map = reactive.Value({})

# Init
self._value: reactive.Value[
None | DataGrid[IntoDataFrameT] | DataTable[IntoDataFrameT]
] = reactive.Value(None)
self._cell_patch_map = reactive.Value({})
# Update the styles within the reactive event so that
# `self._set_cell_patch_map_patches()` does not need to become async
@reactive.effect
@reactive.event(self._cell_patch_map)
async def _update_styles():
await self._attempt_update_cell_style()

def _get_session(self) -> Session:
if self._session is None:
Expand Down Expand Up @@ -661,47 +669,27 @@ async def _patches_handler(self, patches: tuple[CellPatch, ...]) -> Jsonifiable:
)

# Add (or overwrite) new cell patches by setting each patch into the cell patch map
self._set_cell_patch_map_patches(patches)

# With the patches applied, any data retrieved while processing cell style
# will use the new patch values.
await self._attempt_update_cell_style()

# Upgrade any HTML-like content to `CellHtml` json objects
# for sending to the client
processed_patches: list[CellPatchProcessed] = [
{
"row_index": patch["row_index"],
"column_index": patch["column_index"],
# Only upgrade the value if it is necessary
"value": maybe_as_cell_html(
patch["value"],
session=self._get_session(),
),
}
for patch in patches
]

# Prep the processed patches as dictionaries for sending to the client
jsonifiable_patches: list[Jsonifiable] = [
cell_patch_processed_to_jsonifiable(ret_processed_patch)
for ret_processed_patch in processed_patches
]
jsonifiable_processed_patches = self._set_cell_patch_map_patches(patches)

# Return the processed patches to the client
return jsonifiable_patches
return jsonifiable_processed_patches

def _set_cell_patch_map_patches(
self,
patches: ListOrTuple[CellPatch],
):
) -> list[Jsonifiable]:
"""
Set the patches within the cell patch map.
Parameters
----------
patches
Set of patches to apply to store in the cell patch map.
Returns
-------
:
A list of processed (by the session) patches to apply to the data frame.
"""
# Use copy to set the new value
cell_patch_map = self._cell_patch_map().copy()
Expand All @@ -726,46 +714,81 @@ def _set_cell_patch_map_patches(
# Once all patches are set, update the cell patch map with new version
self._cell_patch_map.set(cell_patch_map)

# Upgrade any HTML-like content to `CellHtml` json objects
# for sending to the client
session = self._get_session()
processed_patches: list[CellPatchProcessed] = [
{
"row_index": patch["row_index"],
"column_index": patch["column_index"],
# Only upgrade the value if it is necessary
"value": maybe_as_cell_html(
patch["value"],
session=session,
),
}
for patch in patches
]

# Prep the processed patches as dictionaries for sending to the client
jsonifiable_patches: list[Jsonifiable] = [
cell_patch_processed_to_jsonifiable(ret_processed_patch)
for ret_processed_patch in processed_patches
]

return jsonifiable_patches

async def _attempt_update_cell_style(self) -> None:
with session_context(self._get_session()):

rendered_value = self._value()
if not isinstance(rendered_value, (DataGrid, DataTable)):
return
rendered_value = self._value()
if not isinstance(rendered_value, (DataGrid, DataTable)):
return

styles_fn = rendered_value.styles
if not callable(styles_fn):
return
styles_fn = rendered_value.styles
if not callable(styles_fn):
return

patched_into_data = self._nw_data_to_original_type(self._data_patched())
new_styles = as_browser_style_infos(styles_fn, into_data=patched_into_data)
patched_into_data = self._nw_data_to_original_type(self._data_patched())
new_styles = as_browser_style_infos(styles_fn, into_data=patched_into_data)

await self._send_message_to_browser(
"updateStyles",
{"styles": new_styles},
)
await self._send_message_to_browser(
"updateStyles",
{"styles": new_styles},
)

async def update_cell_value(
self,
value: CellValue,
*,
row_index: int,
column_index: int,
) -> None:
"""
Update the value of a cell in the data frame.
Parameters
----------
value
The new value to set the cell to.
row_index
The row index of the cell to update.
column_index
The column index of the cell to update.
"""
patch: CellPatch = {
"value": value,
"row_index": row_index,
"column_index": column_index,
}
processed_patches = self._set_cell_patch_map_patches([patch])

# TODO-barret-render.data_frame; Send message to client to update cell value
await self._send_message_to_browser(
"addPatches",
{"patches": processed_patches},
)

# TODO-barret-render.data_frame; Add `update_cell_value()` method
# def _update_cell_value(
# self, value: CellValue, *, row_index: int, column_index: int
# ) -> CellPatch:
# """
# Update the value of a cell in the data frame.
#
# Parameters
# ----------
# value
# The new value to set the cell to.
# row_index
# The row index of the cell to update.
# column_index
# The column index of the cell to update.
# """
# cell_patch_processed = self._set_cell_patch_map_patches(
# {value: value, row_index: row_index, column_index: column_index}
# )
# # TODO-barret-render.data_frame; Send message to client to update cell value
# return cell_patch_processed
return

def auto_output_ui(self) -> Tag:
return ui.output_data_frame(id=self.output_id)
Expand Down
8 changes: 4 additions & 4 deletions shiny/www/py-shiny/data-frame/data-frame.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

Loading

0 comments on commit 8eec04d

Please sign in to comment.